最近OpenCVを使った画像処理を嗜んでいる話

本記事は[eeic（東京大学工学部電気電子・電子情報工学科）| Advent Calendar 2022](https://qiita.com/advent-calendar/2022/eeic)の17日目の記事として書かれたものです。

## はじめに
時の流れは早いものでもう学部4年の秋学期、必要な単位も取り終わってあとは卒論を出せば卒業<sup>*1</sup><sup>*2</sup>という季節になりました。
・・・とは言いつつ、せっかくなので私は今学期もいくつか授業を取っています。今回は、そのうちの一つの「OpenCVを用いたプログラミングを通してディジタル画像処理の基礎を学ぶ授業<sup>*3</sup>」で学んだ内容のうち、面白いと思ったものをいくつかをピックアップして、備忘録的にまとめておこうと思います。

※使用言語はPythonです。


## 目次
[はじめに](##-はじめに)
[1. ヒストグラムマッチング](##1-ヒストグラムマッチング)
[2. エンボス](##2-エンボス)
[3. フィルタ処理](##3-フィルタ処理)
[まとめ](##-まとめ)


## 1. ヒストグラムマッチング
#### 画像におけるヒストグラム
画像処理においてヒストグラムは画素値(8bit画像では0~255の256段階)とその頻度のグラフであり、写真に含まれる明るさの分布を表しています。ヒストグラムからは明るさ・暗さやコントラストなど、さまざまな画像の性質を表す情報を得ることができます。
カラー画像では、RGBの3成分のヒストグラムがあります。

<img width="400" alt="gray_img.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/b5cb0a57-7673-4cc3-2502-de77d27562d4.png">

<img width="300" alt="hist1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/4674ea0a-3749-bc41-3baf-8c3d29d7cd87.png">

上の二つの画像は、モノクロ写真とそのヒストグラムを表したものです。また、ヒストグラム上に描かれている青い線は累積ヒストグラムとなっています。

基本的に写真の明るさやコントラストの調節は、元の写真の画素値を色々変化させることによって行われます。例えば上の写真に対してそのヒストグラムを平坦化するような変換(ヒストグラム平坦化)を施すと、次のようになります。

<img width="400" alt="gray_equal.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/7d74bf55-4698-2fa1-d800-08b817e1ac06.png">

<img width="300" alt="hist2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/f38a115d-ced3-3d6f-d16e-4c88659635b2.png">

ヒストグラム平坦化とは画素値が全体的に平らになるようにすることでコントラストを高め、画像を見やすくする効果があります。
※今回用いたモノクロ画像では画素値の分布が元から広かったため、平坦化した後のヒストグラムがあまり平らになっていないですね……。ただ、富士山とかは平坦化によってよりくっきりしていることがわかると思います。

ちなみにこのヒストグラム平坦化は、OpenCVの関数を使うと一行でできてしまいます。

~~~Python
import cv2

img = cv2.imread('image path')
result = cv2.equalizeHist(img)
~~~

#### 本題・ヒストグラムマッチングについて
続いてヒストグラムマッチングに入っていきます。ヒストグラムマッチングとは、2枚の画像A、Bについて、画像Aの中身を保ちながら画像Bのトーンを引き継いだ画像を生成することです。見た方が早いので、以下の例をご覧ください。
<img width="400" alt="histmatchA.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/39e13873-6f0d-6922-75b8-9d0dee53c41d.png"><img width="300" alt="histmatchA_hist.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/c6bf68dc-56f9-ba38-11a1-9c13768d9e7e.png">

<img width="400" alt="histmatchB.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/aa3ef6a6-5f25-59db-0b0b-05c65cc39635.png"><img width="300" alt="histmatchB_hist.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/ee48fd7a-fdfb-1be9-9c7c-c6d80b3f337d.png">

一つ目のの画像に二つ目のトーンを引き継がせると・・・
<img width="400" alt="histmatch_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/30256cde-a0fe-e776-2956-8df39c6176d8.png"><img width="300" alt="histmatch_result_hist.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/582aa237-853f-1d1d-dbba-966a6343cb60.png">

こうなります。
ヒストグラムマッチングの実装の方針は、「内容を引き継ぐ画像Aのヒストグラム」を「トーンを引き継ぐ画像Bのヒストグラム」と同じにすることです。ヒストグラムマッチングという名前の通りですね。上のヒストグラムを見比べてみると、元々の画像のヒストグラムがトーンを引き継ぐ画像のヒストグラムみたいになっていると思います。

ここからは実装の話です。
元の0から255までの画素値がそれぞれどの画素値になればヒストグラムが一致するかを計算し、それを長さ256のルックアップテーブルとして作成することが方針となります。ここでは、A・B二つの画像を累積ヒストグラムを見ながら、Aの累積ヒストグラムをBの累積ヒストグラムに一致するようにルックアップテーブルを作っていきます。
↓がルックアップテーブルを作る一例です。
~~~Python
'''
hist_target : 内容を引き継ぐ画像Aのヒストグラム
hist_tone   : トーンを引き継ぐ画像Bのヒストグラム
'''
cum_target = hist_target.cumsum().astype(np.float64)
cum_target = cum_target / cum_target.max()
cum_tone = hist_tone.cumsum().astype(np.float64)
cum_tone = cum_tone / cum_tone.max()
    
lut = np.zeros(histSize, dtype=np.uint8)
idx_keep = 0
idx_now = 0
while idx_now < histSize:
    if cum_target[idx_now] > cum_tone[idx_now]:
        tmp_n = 1
        while True:
            if idx_now+tmp_n == histSize:
                break
            elif cum_target[idx_now] <= cum_tone[idx_now+tmp_n]:
                break
            tmp_n += 1
        for i in range(tmp_n):
            lut[idx_now+i] = idx_now+tmp_n
        idx_now += 1
    elif cum_target[idx_now] <= cum_tone[idx_now]:
        tmp_n = 1
        while True:
            if idx_now+tmp_n == histSize:
                break
            elif cum_target[idx_now+tmp_n] > cum_tone[idx_now]:
                break
            tmp_n += 1
        for i in range(tmp_n):
            if idx_now+i >= idx_keep:
                lut[idx_now+i] = idx_now
        idx_keep = idx_now + tmp_n
        idx_now += 1
~~~

#### 応用・カラートランスファー
ヒストグラムマッチングをカラー画像のRGBの3チャンネルごとに施すことによって、画像の色転移(カラートランスファー)を行うこともできます。
<img width="400" alt="seaDay.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/3b458f24-491e-08da-3c7b-667dc858b797.jpeg"><img width="400" alt="evening.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/b1acba5c-0d85-e0ad-ee9b-b6ab87a4136c.jpeg">
海の写真に夕焼けの色合いを引き継ぐと、以下のようになります。
<img width="400" alt="seaEvening.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/80766f3a-593f-5fd1-5a72-f1c85169b4a3.png">



## 2. エンボス
エンボスとは、画像の輪郭に陰影をつけることで凹凸感を出す効果のことです。こちらもOpenCVを使って作ることができます。
<img width="400" alt="livehouse.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/bbef44c7-df80-27da-51b2-18dd375d23cf.jpeg"><sup>**1</sup>
↑が今回使う元の写真となります。幻覚が見えている人もいるかもしれません。
画像は[こちら](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwiXs8jTlPv7AhWvklYBHW3QBVkQFnoECA8QAQ&url=https%3A%2F%2Friyot.hatenablog.com%2Fentry%2F2020%2F04%2F14%2F214938&usg=AOvVaw2zVu0S4uTqatVI1nPQAgTd)から引用しました。
これをエンボス化すると、次のようになります。

<img width="400" alt="emboss1_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/5082e44e-cb15-dc5f-e022-99ac9e67a1d6.png">

エンボス画像その１

<img width="400" alt="emboss1_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/14c6e507-b333-fe08-47bc-e33c4df5ec9c.png">

エンボス画像その２、さっきのとは凹凸が逆転しています。

こんな感じです。
やっていることとしては、元の画像をグレースケール化したものと、それを縦横1ピクセルずつずらしたグレースケール画像の差分を取ったものを、グレーの単一階調画像に足したり引いたりすることで凹凸感を出しています。
実装は↓の通りです。
~~~Python
import cv2

img = cv2.imread('image path')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = gray.astype(np.float32) / 255

rows, cols = img.shape[:2]
size = (cols, rows)
original = np.float32(((0,0),(cols,0),(0,rows)))
translate = np.float32(((-1,-1),(cols-1,-1),(-1,rows-1)))
mat = cv2.getAffineTransform(original, translate)
gray_11 = cv2.warpAffine(gray, mat, size)
gray_diff = gray - gray_11

gray_1tone = [[0.5 for _ in range(cols)] for _ in range(rows)]
gray_1tone = np.array(gray_1tone).astype(np.float32)
result = gray_diff * alpha + gray_1tone   # alphaは係数
~~~

uint型よりfloat型にした方が綺麗に凹凸感を出せます。
また他のやり方として、エッジ検出したものをグレー画像に足すなどもあります。

#### 参考：アフィン変換
~~~Python
original = np.float32(((0,0),(cols,0),(0,rows)))
translate = np.float32(((-1,-1),(cols-1,-1),(-1,rows-1)))
mat = cv2.getAffineTransform(original, translate)
gray_11 = cv2.warpAffine(gray, mat, size)
~~~
縦横1ピクセルずつずらした画像を作る際に、アフィン変換を利用しました。
アフィン変換というと、なんとなく回転とか平行移動とかするやつだ〜っていうイメージがあると思います。OpenCVではアフィン変換による画像の平行移動や回転も簡単にできます。
アフィン変換については[こちらの記事<sup>**2</sup>](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwiVgOXkjv77AhX_lFYBHX7TDmMQFnoECAwQAQ&url=https%3A%2F%2Fqiita.com%2Fkoshian2%2Fitems%2Fc133e2e10c261b8646bf&usg=AOvVaw2mJoaCRhla7kFuSCVXJVFY)がわかりやすくまとまっています。

## 3. フィルタ処理(空間フィルタリング)
最後はフィルタ処理の例をいくつか紹介します。3Aの某講義とかでも出てきましたね。画像フィルタには空間フィルタリングと周波数フィルタリングの2種類がありますが、ここでは空間フィルタリングについて扱います。周波数フィルタリングについてはまだ習っていないので・・・。
OpenCVを使えば、画像にフィルタ処理を施すのも超簡単です。

<img width="400" alt="dekanu.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/9e83c855-3c0a-9d3b-c630-118ab4b9a2c9.jpeg">

こちらがフィルタ処理において使用する画像です。
パルデア地方の冒険でお世話になったデカヌチャン、かわいい見た目と凶悪な生態のギャップがいいですよね。ポケモンSVでのお気に入りの一体です。

#### エッジ検出
<img width="400" alt="dekanu_edge.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/9350d765-07a5-8e0d-d580-1df288fc9775.png">

ラプラシアンフィルタを適用してエッジ検出をしています。
~~~Python
import cv2

img = cv2.imread('image path')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
result = cv2.Laplacian(gray, cv2.CV_64F, ksize=3)
~~~

#### ぼかし
<img width="400" alt="dekanu_gaussian.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/f77484a4-2148-3ec5-1466-feb41ddb7d32.png">

ガウシアンフィルタを適用すればぼかし加工をすることもできます。
~~~Python
import cv2

img = cv2.imread('image path')
result = cv2.GaussianBlur(img, (ksize, ksize), sigma)
'''
ksize : カーネルのサイズ
sigma : ガウス分布の標準偏差
'''
~~~

#### 鮮鋭化

OpenCVには色々なフィルタ関数が入っているので、それらを使えば簡単にフィルタ処理ができてしまいますが、自分で作ったカーネルでフィルタ処理をすることもできます。
ここでは画像のエッジを強調する鮮鋭化を例に、自作のカーネルによるフィルタ処理を実際にやってみたいと思います。

<img width="400" alt="dekanu_sharpen.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3004330/11752d23-6e0b-beed-51fa-8fd8dd606a1b.png">

エッジが強調されるとまた雰囲気が変わりますよね。
実装は↓のようになっています。
~~~Python
import cv2
import numpy as np

img = cv2.imread('image path')

# 3*3カーネル
kernel = [[-1/9,  -1/9, -1/9],
          [-1/9, 1+8/9, -1/9],
          [-1/9,  -1/9, -1/9]]
kernel = np.array(kernel, np.float32)

result = cv2.filter2D(img, -1, kernel)
~~~

## まとめ
OpenCVでの画像処理について簡単に書いてみました。誰かの役に立つことがあれば嬉しい限りです。　

eeicの講義・実験でも画像処理やOpenCVを扱うものはありましたが、当時は他にも色々な講義や実験があったこともあってあまり身につきませんでした()。多少時間が取れるようになった今、改めて画像処理について学んでみると新たな発見があったりします。今後は個人的に興味のある機械学習などにおいてOpenCVを使っていけたらと思っています。そしてまた何かにまとめたくなったら記事にして投稿しようと思います。

### 感想

・・・なんか思ったより浅い感じになっちゃいました。本当はまだまだ書きたい内容があったのですが、アドベントカレンダーには如何せん締め切りというものがあるのでここまでにします。
最初は「Qiitaに記事書いたことないし面白そうだからやってみようかな」という軽い気持ちで参加したアドベントカレンダーですが、なんだかんだで先延ばしにし、気づいたら締め切りギリギリになって焦る始末。自分の計画性の無さを再確認しました。この反省を踏まえ、卒論は焦ることなく書き終えたいですね・・・。
それでは私はもう寝ようと思います。おやすみなさい。


### 脚注
*1: **卒論を出すことができれば**卒業できます。
*2: 人によってはまだ単位が足りてないかもしれませんが、悪しからず。
*3: なんとなく講義名は伏せておきますが、eeicで開講されているものではありません。

### 引用
**1:[ぼっち・ざ・ろっく！聖地巡礼](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwiXs8jTlPv7AhWvklYBHW3QBVkQFnoECA8QAQ&url=https%3A%2F%2Friyot.hatenablog.com%2Fentry%2F2020%2F04%2F14%2F214938&usg=AOvVaw2zVu0S4uTqatVI1nPQAgTd)
**2:[完全に理解するアフィン変換](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwiVgOXkjv77AhX_lFYBHX7TDmMQFnoECAwQAQ&url=https%3A%2F%2Fqiita.com%2Fkoshian2%2Fitems%2Fc133e2e10c261b8646bf&usg=AOvVaw2mJoaCRhla7kFuSCVXJVFY)