Pixel4のカメラで学ぶDepth map-2(Dark Shading補正)

#はじめに
 この記事は[Note](https://note.com/sv_engineer/m/m98195ca0c6e1)で連載している記事で扱っているCode部分のみを切り出しています。技術背景等にご興味がある方はNoteの記事の方をご参照ください。
　一部のコードは参考文献を基に少し手を加えたものを使用しております。手を加えただけではOriginalityは自分にはないと判断し、Codeは載せておりません（読みにくくて申し訳ございません）。あくまで自分が一から作成した部分のみを今回記載しています。参考文献は非常に勉強になりますしCodeの実装方法に関しても詳しく書かれておりますので是非とも参考にしてください。

[PythonとColabでできる-ゼロから作るRAW現像](moiz.booth.pm/items/1292480)
![638e8eb0-da18-40b2-aa20-8366051eaff2_base_resized.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28386/c4a96f3c-945d-ed73-e972-de53ee32f84e.jpeg)

#画像の下処理の重要性
　機械学習や他の画像処理もそうですか、カメラから得られた画像をそのまま使うことはほぼありません。理由はそのままの素の状態ではノイズや意図しない情報や画像間でのばらつきがあり、それらの影響で意図した結果が得られない可能性が高いからです。そのためいくつかの処理を実施し、ばらつきや意図した結果が得られるようにする必要があります。

#実施する下処理の種類
　以下の下処理を順に処理していきます。これらの処理はDark classにまとめています。以下に出てくる関数等は基本的にDark classのMethodとして実装してあります。
　

<実施する処理>
1. Dark shading補正
2. Noise処理
3. Edge強調処理
4. 規定のFormatへ変換し出力

#Dark Shading補正とは
　スマホや安いModuleカメラで撮影するとレンズの影響により画像端部分の出力値が落ちてしまいます。このまま画像を使用すると画像端部分が暗いままになるため、あらかじめ画像端部分にGainを印加し、出力を持ち上げる必要があります。この持ち上げる処理のことをDark Shading補正と呼びます。上記の参考資料をもとにDual-Pixel用に加工したものがこちらとなります。

###画像中心からの半径距離抽出
 まず初めに、基準となる画像からshadingのプロファイルを抽出しするところから始めます。きちんとした設備で撮影すれば光軸合わせ等ができるのですが、そこまで施設がない場合を想定し、ある程度自由度をもたした関数を作成しました。

```python:dark.py
def _calc_radials(self, kernel_size: int = 32, \
                 offset_x: int=0, offset_y: int=0) ->[int]:
         ''' Calculate radial
         Paramters
         ---------
         kernal_size: int
          kernel size, default value is 32
         offset_x: int
          offset value for x-axis, to give the function center positioning flexibility
         offset_y: int
          offset value for y-axis, to give the function center positioning flexibility

        Returns
        -------
        radials: [int]
         radial of each position
        '''
        radials = []
        for y in range(0, self._img_height, kernel_size):
            for x in range(0, self._img_width - kernel_size, kernel_size):
　　　　　　　　# Coreの計算部分は、ほぼ参考資料(p60~)と同じなので参考文献をご参照ください
　　　　　　　　# この部分は割愛いたします。

        # return
        return radials
```
###定義した画像内各ブロックの平均値抽出
　次に、基準画像から各ポイントの値を抽出する関数です。参考文献を参照しkernel内部の平均値を計算しています。参考文献と異なる点は、PGM画像はBayer配列ではなく、輝度情報のみなので配列からのData取り出しが簡略化されています。

```python:dark.py
def _extract_shading_profile(self, img: np.ndarray, kernel_size: int = 32):
        ''' Extract shading profile from image file
        Paramers
        ---------
        img: np.ndarray
         Reference image input
        kernel_size: int
         Kernel size for profile extraction, the number should match with the kernel size in radial calculation
        Returns
        -------
        val: array
         extracted profile values
        '''
        val = [[], [], []]
        for y in range(0, self._img_height, kernel_size):
            for x in range(0, self._img_width - kernel_size, kernel_size):
　　　　　　　　# Coreの計算部分は、ほぼ参考資料(p61~)と同じなので参考文献をご参照ください
　　　　　　　　# この部分は割愛いたします。
        return val
```
###Profileの近似曲線計算
　次はRadialsと抽出した基準画像のProfileを用いて、近似曲線を作成します。こちらも参考資料を基に一部改造したものとなります。

```python:dark.py
    def _proximate_shading(self, radials: [int], profiles: []):
       ''' priximate shading profile from data
       Parameters
       -----------
       radials: [int]
        input data of radial
       profiles: []
        input data of profiles
       Returns
       -------
       '''
　　　　# Coreの計算部分は、ほぼ参考資料(p63~)と同じなので参考文献をご参照ください
　　　　# この部分は割愛いたします。
```
###Gain Map計算
　ここまで来たらもう少しです。上記の近似計算結果を基に実際のGain mapを作成します。
汎用性を考え、画像全体へのGain印加を加味した作りにしました。稀に暗いところで撮影すると画像全体にAnalog Gainが印加されるのでそれを考慮したものです。
　今まで実装してきた関数を呼び出してDataの流れを作り、それらを統合して最終的なGain mapを計算して出力する構成にしています。Leftなのか、Rightなのかを判別しています。理由は後で説明します。

```python:dark.py
def _gain_map(self, left: bool = True, kernal_size: int=32, \
        analog_gain= [1.0, 1.0, 1.0], offset=(0, 0)) -> np.ndarray:

        img = self._left_img
        if not left:
            img = self._right_img
        
        # calculate radials
        offset_x, offset_y = offset
        radials = self._calc_radials(kernal_size, offset_x=offset_x, offset_y=offset_y)

        # extract raw data profiles
        profiles = self._extract_shading_profile(img, kernel_size=kernal_size)

        # left gain map
        self._proximate_shading(radials=radials, profiles=profiles)

        # create gain map
        return self._create_dksh_gain_map(analog_gain=analog_gain)
```
　色々と実験して分かったのがLeft画像とRgight画像でDark shading用のGain mapを変えない幾つか不具合が発生することです。なのでDark shading処理をする画像がLeftなのかRightなのかを予め指定する必要があります。それを考慮し実装したのが以下となります。

```python:dark.py
def get_gain_map(self, kernal_size: int=32, analog_gain= [1.0, 1.0, 1.0], 
    left_offset=(0, 0), right_offset=(0, 0)) -> (np.ndarray, np.ndarray):
        # left side gain map
        left_gain_map = self._gain_map(left=True, kernal_size=kernal_size, \
            analog_gain=analog_gain, offset=left_offset)

        # right side gain map
        right_gain_map = self._gain_map(left=True, kernal_size=kernal_size, \
            analog_gain=analog_gain, offset=right_offset)

        return left_gain_map, right_gain_map
```
###Dark classの本体定義
　簡単ですが、Dark shading補正部分の実装を説明してきました。最後にDark classの定義を示し終わりたいと思います。
 

```python:dark.py
import cv2
import numpy as np
import helper

class LinearPoly():
    def __init__(self):
        self.slope = 0.0
        self.offset = 0.0

class Dark():
    """ Dark image handler
    """
    def __init__(self, path: str, ext: str='pgm', dsize= (2016, 1512)):
        # make file path list
        self._file_path_list = helper.make_file_path_list(path, ext)
        self._file_path_list.sort()

        # image size
        self._img_width, self._img_height = dsize
        self._img_center_x = self._img_width // 2
        self._img_center_y = self._img_height // 2

        # read dark images
        self._dark_imgs = helper.read_img_from_path(self._file_path_list, self._img_width, self._img_height)

        # recognize postion left or right
        for idx, loc in enumerate(self._file_path_list):
            if helper.loc_detector_from_name(loc):
                self._left_img = self._dark_imgs[idx]
            else:
                self._right_img = self._dark_imgs[idx]

        # dark shading fitting data
        self._dksh_para = [LinearPoly(), LinearPoly(), LinearPoly()]
```