python + OpenCVで画像を加工③ 画像にモザイク・ぼかしをかける

####今回の目標
前回の記事で顔を検出できるようになったので、次の目標は検出した顔にモザイクをかけることです。その前哨戦として、今回は画像全体にモザイクとぼかしをかけてみようと思います。

使用する画像はこちら。
![pumpkin.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634245/f1b3db19-bc45-1353-16cb-9f888e0816b1.jpeg)

####モザイク

まずは画像にモザイクをかけてみようと思います。最終的なコードを最初に掲載しておきます。

```python
import cv2

img = cv2.imread('photo.jpg')

def mosaic(img, ratio):
    small = cv2.resize(img, None, fx=ratio, fy=ratio)
    return cv2.resize(small, img.shape[:2][::-1])

cv2.imshow('photo_mosaic.jpg', mosaic(img, ratio=0.1))
cv2.waitKey()
```
imread()やresize()については[前々回の記事](https://qiita.com/HIROKOBA/items/d1b076bef22932be5eea)で触れておりますので必要でしたら参照なさってください。

```python
img = cv2.imread('photo.jpg')
```
↑まずはimread()で画像を読み取ってimg変数に代入します。

```python
def mosaic(img, ratio):
   small = cv2.resize(img, None, fx=ratio, fy=ratio)
   return cv2.resize(small, img.shape[:2][::-1])
```
↑今回の肝、モザイク処理を行う関数です。

```python
   small = cv2.resize(img, None, fx=ratio, fy=ratio)
```
↑この１行で、いったん画像を10分の1のサイズに縮小しています。10分の1の指定はこの後に出てくる

```python
cv2.imshow('photo_mosaic.jpg', mosaic(img, ratio=0.1))
```
・・・の ratio=0.1 で指定しています(例えば縦横半分の大きさにしたいならratio=0.5を指定する)。

どのくらい小さくなったのかを確認してみると。

元の画像：71.8kb
![pumpkin.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634245/22f3f9c6-ec9c-2dbf-a21c-7752742893ff.jpeg)
縮小した画像：3.16kb
![aaa.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634245/28fa0a0a-435d-8318-7e3e-73e0215cf41f.jpeg)
・・・だいぶ小さくなりました。

(この分野の専門ではないので下記の表現が正しいか分かりませんが)縮小した写真は画素数が減った状態なので、無理やり元の大きさに戻せば、少ない画素数で空間を埋めようとするためいわゆる「モザイク」の状態になるはず、という理屈です。

```python
def mosaic(img, ratio):
   small = cv2.resize(img, None, fx=ratio, fy=ratio)
   return cv2.resize(small, img.shape[:2][::-1])
```
さきほどの関数の

```python
   return cv2.resize(small, img.shape[:2][::-1])
```
この１行で、縮小された画像を元の大きさに戻し、mosaic関数の戻り値を返す仕組みです。ただ、下記の表記にたどりつくまでにえらい苦労が。。。

```python
   img.shape[:2][::-1]
```
これ、(縮小する前の)画像の縦横の大きさを指定しています。縮小する前のサイズをprint()で確認してみると。

```
print(img.shape[:2])

#出力結果
#(427, 640)
```
2つの要素が画像の大きさを示しているので、img.shape[:2]で縦横の大きさを指定して、これでいける！と思ってコードを実行すると・・・
![photo_mosaic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634245/a99364a1-60da-c123-0ad7-589bcb311db7.jpeg)
モザイクはかかっていますが、なぜか縦長の画像が出力されました。

先ほどの出力結果をもう一度見て見入ると。

> (427, 640)

最初の２つの数字が、縦→横の順番になっているではありませんか。一方で、resize()で大きさを指定する順番は横→縦の順番。これは困りました。

タプルの順番を入れ替えるための表記を調べて、ようやくうまくいったのが先ほど掲載した

```python
   return cv2.resize(small, img.shape[:2][::-1])
```
・・・の表記だったという次第です。[::-1]で縦と横の順番を逆にしています。
例）

```python
img = cv2.imread('photo.jpg')
reverse_order = img.shape[:2][::-1]
print(reverse_order)

#出力結果
#(640, 427)
```

[::-1]の記述は恥ずかしながら今まで知りませんでした。。

```python
cv2.imshow('photo_mosaic.jpg', mosaic(img, ratio=0.1))
cv2.waitKey()
```
そしてこのコードの

```python
mosaic(img, ratio=0.1)
```
で先ほど定義したmosaic関数を呼び出して、結果を表示したという次第です。imshow()で画像を表示し続けるためにwaitKey()を最後に記述しています。
これでコードを実行すると・・・
![photo_mosaic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634245/c8fe47bf-dbeb-6025-276d-b8b17ee6d9b5.jpeg)
うまくいきました！
最後に完成コードをもう一度。

```python
import cv2

img = cv2.imread('photo.jpg')

def mosaic(img, ratio):
    small = cv2.resize(img, None, fx=ratio, fy=ratio)
    return cv2.resize(small, img.shape[:2][::-1])

cv2.imshow('photo_mosaic.jpg', mosaic(img, ratio=0.1))
cv2.waitKey()
```
imread()で読み込んだ数値を吐き出すと、画像の大きさ表示が縦→横の順番になっているのがトリッキーでした。

####平滑化＝ぼかし
引き続き、モザイクのようにガビガビした感じではなく、ピントが合ってない感じにしたいと思います。専門用語では「平滑化」という言葉を用いるようです。[オープンCVのチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_filtering/py_filtering.html)に詳しい解説が乗っています。

> 平均を取るには正規化された箱型フィルタを使います． カーネルの範囲内にある全画素の画素値の平均をとります．

ただどういうアルゴリズムなのか調べても、数式がざっと並んでさっぱり分かりません・・・とりあえずチュートリアルの冒頭に紹介されている、画像を平均化する方法でやってみようと思います。

画像にぼかしをかける全体のコードはこちら。シンプルです。

```python
import cv2
img = cv2.imread('pumpkin.jpg')

#画像を平均化して(＝画像にぼかしをかけて)img_blurに格納する
def average(img):
    img_blur = cv2.blur(img, (17, 17))
    return img_blur

cv2.imshow("pumpkin_averaged.jpg", average(img))
cv2.waitKey()
cv2.destroyAllWindows()
```
```python
def average(img):
    img_blur = cv2.blur(img, (17, 17))
    return img_blur
```
こちらが画像にぼかしをかけている部分です。ただ[公式ドキュメント](https://docs.opencv.org/4.5.3/d4/d86/group__imgproc__filter.html)には引数についていろいろ紹介してあるのですが、ここに書いてある引数を試そうとしてもことごとくエラーが発生してしまいます。。。他の記事も読んでみた結論としては、blur()関数について必要な引数は２つ。

①加工をする対象の画像
②カーネルのサイズ

平均化というのをかいつまんで言うと「画像の１点とその周囲のピクセル値を平均して中心のピクセル値として出力する」くらいの意味で、平均化するのに参照する範囲のことをカーネルと呼んでいるようです。

要するに「カーネルの値が大きいほどぼかしが濃くなる」と覚えておけば問題なさそう。OpenCVのチュートリアルによると
> 平均を取るには正規化された箱型フィルタを使います． カーネルの範囲内にある全画素の画素値の平均をとります．
カーネルの縦幅，横幅を指定する必要が有ります．

・・・とのことですが、とりあえずやってみたのが

```python
    img_blur = cv2.blur(img, (17, 17))
```
この１行でした。
![pumpkin_averaged.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634245/5025cf1b-c696-257c-151c-f782fd81683f.jpeg)
ガビガビではなく、ピントがずれた感じになりました。平滑化の処理はかなり奥が深そうなので、今後きちんと使えるようになりたいと思います。

次は検出した顔にモザイクやぼかしをかけてみようと思っています。
お読みいただきありがとうございました。

参考にさせていただいた記事
[OpenCVで画像の平滑化をしてみた](https://qiita.com/shoku-pan/items/07ec25f1d50629fed698)