OpenCV + PySimpleGUIで特定の色を消す

# はじめに
 OpenCVを有効活用するには、一つの処理だけではなく、複数の処理や、ほかのパッケージとの組み合わせにより求める結果を生み出していくことになります。今回は例として、クリックした部分の色を消す処理を行ってみます。


# この記事でできること
動画内の特定の色の部分を左クリックするとその色が消えます。消す部分の大きさや、ぼかし具合を調整するには、HSV値などのパラメータを変更する必要があります。パラメータはGUIで変更できるようにしています。

![test_normal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205544/724c2633-df31-0569-94c9-9f20b060b32f.gif)

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205544/eb5a776a-d1b4-f247-a284-bf24ea1a7d71.gif)


# 処理の流れ
以下の流れで処理をしています。
動画の読込

↓

マスク作成
→　 画像をHSV変換し、特定の色にのみマスクをかけるように設定します。
　　色の抽出は動画の上を左クリックすることでその部分の色を抽出します。
　　加えてPySimpleGUIで作成したスライダでも微調整できるようにしています。

↓


マスク画像へノイズ除去 (Opening, Closing)
→   色の抽出のみではノイズが残るため、Opening, Closing処理でノイズを除去します。

↓

マスク画像へ膨潤処理 (Dilation)
→   色の輪郭部分はうまく抽出できない場合が多いため、マスク部分を一様に広げる処理を行います。

↓

マスク部分に補修処理 (Inpaint)
→   マスク部分を周りの色で補修する処理を行います。

↓

マスク部分にぼかし(Blur)
→    Inpaintのみでは目立つため、ぼかし処理を入れています。

↓

表示


# プログラム

## 動画の読込
```python
import PySimpleGUI as sg
import cv2
import numpy as np
from pathlib import Path


def file_read():
    '''
    ファイルを選択して読み込む
    '''
    fp = ""
    # GUIのレイアウト
    layout = [
        [
            sg.FileBrowse(key="file"),
            sg.Text("ファイル"),
            sg.InputText()
        ],
        [sg.Submit(key="submit"), sg.Cancel("Exit")]
    ]
    # self.WINDOWの生成
    window = sg.Window("ファイル選択", layout)

    # イベントループ
    while True:
        event, values = window.read(timeout=100)
        if event == 'Exit' or event == sg.WIN_CLOSED:
            break
        elif event == 'submit':
            if values[0] == "":
                sg.popup("ファイルが入力されていません。")
                event = ""
            else:
                fp = values[0]
                break
    window.close()
    return Path(fp)

```

## HSVによる色検出
指定した範囲内のHSV値の部分のみを処理対象にするためのマスクを作成する関数です。PySimpleGUI上で指定した
スライダの値を読み取り、マスク画像を生成します。


```python

def hsv(frame, H_max, H_min, S_max, S_min, V_max, V_min, reverse=False):
    frame_hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    if reverse:
        lower1 = np.array([0, int(S_min), int(V_min)])
        upper1 = np.array([int(H_min), int(S_max), int(V_max)])
        mask1 = cv2.inRange(frame_hsv, lower1, upper1)
        lower2 = np.array([int(H_max), int(S_min), int(V_min)])
        upper2 = np.array([255, int(S_max), int(V_max)])
        mask2 = cv2.inRange(frame_hsv, lower2, upper2)
        mask = mask1 + mask2
        frame = cv2.bitwise_and(frame, frame, mask=mask)
        # mask = cv2.bitwise_and(frame, mask, mask=mask)

    else:
        lower = np.array([int(H_min), int(S_min), int(V_min)])
        upper = np.array([int(H_max), int(S_max), int(V_max)])
        mask = cv2.inRange(frame_hsv, lower, upper)
        frame = cv2.bitwise_and(frame, frame, mask=mask)

    return frame
```


##　画像の読込とGUI
ファイルから動画を読み込み、OpenCVで表示します。
画像上を左クリックしたとき、その部分のHSV値をPySimpleGUIのスライダの値に反映させる関数を作成し、コールバック関数に指定します。

```python
class Main:
    def __init__(self):
        self.fp = file_read()
        self.cap = cv2.VideoCapture(str(self.fp))

        # 動画の保存フラグ
        self.rec_flg = False

        # 1フレーム目の取得
        # 取得可能かの確認
        self.ret, self.f_frame = self.cap.read()
        self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
        # フレームが取得できた場合、各種パラメータを取得
        if self.ret:
            self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
            # 動画情報の取得
            self.fps = self.cap.get(cv2.CAP_PROP_FPS)
            self.org_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            self.width = self.org_width
            self.org_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            self.height = self.org_height
            self.total_count = self.cap.get(cv2.CAP_PROP_FRAME_COUNT)

            # マスク画像の定義
            self.mask = np.zeros_like(self.f_frame[:, :, 0])

            # フレーム関係
            self.frame_count = 0
            self.s_frame = 0
            self.e_frame = self.total_count

            # 再生の一時停止フラグ
            self.stop_flg = False

            # GUIのイベント
            self.event = ""

            cv2.namedWindow("Movie")

            # マウスイベントのコールバック登録
            cv2.setMouseCallback("Movie", self.onMouse)
        # フレームを取得出来なかった場合に終了する
        else:
            sg.Popup("ファイルの読込に失敗しました。")
            return

    # マウスイベント
    def onMouse(self, event, x, y, flags, param):
        if event == cv2.EVENT_LBUTTONUP:
            hsv = cv2.cvtColor(
                self.frame[y:y + 1, x:x + 1, :], cv2.COLOR_BGR2HSV)
            h = int(hsv[:, :, 0])
            h_min = max(h - 20, 0)
            h_max = min(255, h + 20)
            s = int(hsv[:, :, 1])
            s_min = max(s - 20, 0)
            s_max = min(255, s + 20)
            v = int(hsv[:, :, 2])
            v_min = max(v - 20, 0)
            v_max = min(255, v + 20)
            self.window['-H_MIN SLIDER_MASK-'].update(h_min)
            self.window['-H_MAX SLIDER_MASK-'].update(h_max)
            self.window['-S_MIN SLIDER_MASK-'].update(s_min)
            self.window['-S_MAX SLIDER_MASK-'].update(s_max)
            self.window['-V_MIN SLIDER_MASK-'].update(v_min)
            self.window['-V_MAX SLIDER_MASK-'].update(v_max)
            self.window['-Hue Reverse_MASK-'].update(False)

    def run(self):
        # GUI #######################################################
        # GUIのレイアウト

        layout = [
            [
                sg.Text("Start", size=(8, 1)),
                sg.Slider(
                    (0, self.total_count - 1),
                    0,
                    1,
                    orientation='h',
                    size=(45, 15),
                    key='-START FRAME SLIDER-',
                    enable_events=True
                )
            ],
            [sg.Slider(
                (0, self.total_count - 1),
                0,
                1,
                orientation='h',
                size=(50, 15),
                key='-PROGRESS SLIDER-',
                enable_events=True
            )],
            [sg.HorizontalSeparator()],
            [
                sg.Text("Resize     ", size=(13, 1)),
                sg.Slider(
                    (0.1, 4),
                    1,
                    0.01,
                    orientation='h',
                    size=(40, 15),
                    key='-RESIZE SLIDER-',
                    enable_events=True
                )
            ],
            [
                sg.Checkbox(
                    "Inpaint",
                    size=(10, 1),
                    default=False,
                    key='-INPAINT-',
                    enable_events=True
                )
            ],
            [
                sg.Checkbox(
                    "Opening",
                    size=(20, 1),
                    default=False,
                    key='-OPENING-',
                    enable_events=True
                ),
                sg.Checkbox(
                    "Closing",
                    size=(20, 1),
                    default=False,
                    key='-CLOSING-',
                    enable_events=True
                ),
                sg.Slider(
                    (3, 31),
                    5,
                    2,
                    orientation='h',
                    size=(15, 15),
                    key='-OPENING SLIDER-',
                    enable_events=True
                )
            ],
            [
                sg.Checkbox(
                    "Dilation",
                    size=(10, 1),
                    default=False,
                    key='-DILATION-',
                    enable_events=True
                ),
                sg.Slider(
                    (1, 31),
                    4,
                    2,
                    orientation='h',
                    size=(15, 15),
                    key='-DILATION SLIDER-',
                    enable_events=True
                )
            ],
            [
                sg.Checkbox(
                    'blur',
                    size=(10, 1),
                    key='-BLUR-',
                    enable_events=True
                ),
                sg.Slider(
                    (1, 10),
                    1,
                    1,
                    orientation='h',
                    size=(40, 15),
                    key='-BLUR SLIDER-',
                    enable_events=True
                )
            ],
            [
                sg.Text(
                    'hsv',
                    size=(10, 1),
                    key='-HSV_MASK-',
                    enable_events=True
                ),
                sg.Button('Blue', size=(10, 1)),
                sg.Button('Green', size=(10, 1)),
                sg.Button('Red', size=(10, 1))
            ],
            [
                sg.Checkbox(
                    'Hue Reverse',
                    size=(10, 1),
                    key='-Hue Reverse_MASK-',
                    enable_events=True
                )
            ],
            [
                sg.Text('Hue', size=(10, 1), key='-Hue_MASK-'),
                sg.Slider(
                    (0, 255),
                    0,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-H_MIN SLIDER_MASK-',
                    enable_events=True
                ),
                sg.Slider(
                    (1, 255),
                    125,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-H_MAX SLIDER_MASK-',
                    enable_events=True
                )
            ],
            [
                sg.Text('Saturation', size=(10, 1), key='-Saturation_MASK-'),
                sg.Slider(
                    (0, 255),
                    50,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-S_MIN SLIDER_MASK-',
                    enable_events=True
                ),
                sg.Slider(
                    (1, 255),
                    255,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-S_MAX SLIDER_MASK-',
                    enable_events=True
                )
            ],
            [
                sg.Text('Value', size=(10, 1), key='-Value_MASK-'),
                sg.Slider(
                    (0, 255),
                    50,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-V_MIN SLIDER_MASK-',
                    enable_events=True
                ),
                sg.Slider(
                    (1, 255),
                    255,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-V_MAX SLIDER_MASK-',
                    enable_events=True
                )
            ],
            [sg.Output(size=(65, 5), key='-OUTPUT-')],
            [sg.Button('Clear')]
        ]

        # self.Windowを生成
        self.window = sg.Window('OpenCV Integration', layout, location=(0, 0))
        # 動画情報の表示
        self.event, values = self.window.read(timeout=0)
        print("ファイルが読み込まれました。")
        print("File Path: " + str(self.fp))
        print("fps: " + str(int(self.fps)))
        print("width: " + str(self.width))
        print("height: " + str(self.height))
        print("frame count: " + str(int(self.total_count)))

    # メインループ #########################################################
        try:
            while True:
                # GUIイベントの読込
                self.event, values = self.window.read(
                    timeout=0
                )

                # イベントをウィンドウに表示
                if self.event != "__TIMEOUT__":
                    print(self.event)
                # Exitボタンが押されたら、またはウィンドウの閉じるボタンが押されたら終了
                if self.event in ('Exit', sg.WIN_CLOSED, None):
                    break

                # 動画の再読み込み
                # スタートフレームを設定していると動く
                if self.event == 'Reset':
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
                    self.frame_count = self.s_frame
                    self.window['-PROGRESS SLIDER-'].update(self.frame_count)

                    self.video_stabilization_flg = False
                    self.stab_prepare_flg = False

                    # Progress sliderへの変更を反映させるためにcontinue
                    continue

                # フレーム操作 ################################################
                # スライダを直接変更した場合は優先する
                if self.event == '-PROGRESS SLIDER-':
                    # フレームカウントをプログレスバーに合わせる
                    self.frame_count = int(values['-PROGRESS SLIDER-'])
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                # スタートフレームを変更した場合
                if self.event == '-START FRAME SLIDER-':
                    self.s_frame = int(values['-START FRAME SLIDER-'])
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
                    self.frame_count = self.s_frame
                    self.window['-PROGRESS SLIDER-'].update(self.frame_count)

                # カウンタがエンドフレーム以上になった場合、スタートフレームから再開
                if self.frame_count >= self.e_frame:
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
                    self.frame_count = self.s_frame
                    self.window['-PROGRESS SLIDER-'].update(self.frame_count)
                    continue

                # ストップボタンで動画の読込を一時停止
                if self.event == 'Play / Stop':
                    self.stop_flg = not self.stop_flg

                # ストップフラグが立っており、eventが発生した場合以外はcountinueで
                # 操作を停止しておく

                # ストップボタンが押された場合は動画の処理を止めるが、何らかの
                # eventが発生した場合は画像の更新のみ行う
                # mouse操作を行っている場合も同様
                if(
                    (
                        self.stop_flg
                        and self.event == "__TIMEOUT__"
                    )
                ):
                    self.window['-PROGRESS SLIDER-'].update(self.frame_count)
                    continue

                # フレームの読込 ##############################################
                self.ret, self.frame = self.cap.read()
                # 最後のフレームが終わった場合self.s_frameから再開
                if not self.ret:
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
                    self.frame_count = self.s_frame
                    continue

                # リサイズ
                self.width = int(self.org_width * values['-RESIZE SLIDER-'])
                self.height = int(self.org_height * values['-RESIZE SLIDER-'])
                self.frame = cv2.resize(self.frame, (self.width, self.height))

                # ROIに対して処理を実施 ##########################################
                if self.event == 'Blue':
                    self.window['-H_MIN SLIDER_MASK-'].update(70)
                    self.window['-H_MAX SLIDER_MASK-'].update(110)
                    self.window['-S_MIN SLIDER_MASK-'].update(70)
                    self.window['-S_MAX SLIDER_MASK-'].update(255)
                    self.window['-V_MIN SLIDER_MASK-'].update(0)
                    self.window['-V_MAX SLIDER_MASK-'].update(255)
                    self.window['-Hue Reverse_MASK-'].update(False)

                if self.event == 'Green':
                    self.window['-H_MIN SLIDER_MASK-'].update(20)
                    self.window['-H_MAX SLIDER_MASK-'].update(70)
                    self.window['-S_MIN SLIDER_MASK-'].update(70)
                    self.window['-S_MAX SLIDER_MASK-'].update(255)
                    self.window['-V_MIN SLIDER_MASK-'].update(0)
                    self.window['-V_MAX SLIDER_MASK-'].update(255)
                    self.window['-Hue Reverse_MASK-'].update(False)

                if self.event == 'Red':
                    self.window['-H_MIN SLIDER_MASK-'].update(20)
                    self.window['-H_MAX SLIDER_MASK-'].update(110)
                    self.window['-S_MIN SLIDER_MASK-'].update(70)
                    self.window['-S_MAX SLIDER_MASK-'].update(255)
                    self.window['-V_MIN SLIDER_MASK-'].update(0)
                    self.window['-V_MAX SLIDER_MASK-'].update(255)
                    self.window['-Hue Reverse_MASK-'].update(True)

                self.mask = self.frame
                self.mask = hsv(
                    self.mask,
                    values['-H_MAX SLIDER_MASK-'],
                    values['-H_MIN SLIDER_MASK-'],
                    values['-S_MAX SLIDER_MASK-'],
                    values['-S_MIN SLIDER_MASK-'],
                    values['-V_MAX SLIDER_MASK-'],
                    values['-V_MIN SLIDER_MASK-'],
                    values['-Hue Reverse_MASK-']
                )

```
## マスク画像への処理
マスク画像のグレイスケール化、Opening処理、Closing処理、Dilation処理を行います。


```python


                # グレイスケール
                self.mask = cv2.cvtColor(
                    self.mask,
                    cv2.COLOR_BGR2GRAY
                )

                # ノイズ除去
                if values['-OPENING-']:
                    self.mask = cv2.morphologyEx(self.mask, cv2.MORPH_OPEN,
                        np.ones((int(values['-OPENING SLIDER-']), int(values['-OPENING SLIDER-'])), np.uint8))

                # ノイズ除去2
                if values['-CLOSING-']:
                    self.mask = cv2.morphologyEx(self.mask, cv2.MORPH_CLOSE,
                        np.ones((int(values['-OPENING SLIDER-']), int(values['-OPENING SLIDER-'])), np.uint8))

                # 膨張処理
                if values['-DILATION-']:
                    self.mask = cv2.dilate(self.mask,
                        np.ones((int(values['-DILATION SLIDER-']), int(values['-DILATION SLIDER-'])), np.uint8), iterations=1)

```

## フレームへの補修処理、ぼかし処理
マスク部分へのInpaint処理、Blur処理を行います。Inpaint処理を掛ける部分は引数にマスク画像を指定することで実施できます。Blurについては、`cv2.bitwise_not`を使用してマスク部分にのみBlurを掛けています。

```python

                if values['-INPAINT-']:
                    self.frame = cv2.inpaint(
                        self.frame,
                        self.mask,
                        2,
                        cv2.INPAINT_TELEA
                    )

                # ぼかし
                if values['-BLUR-']:
                    self.frame_roi = cv2.GaussianBlur(
                        self.frame, (21, 21), values['-BLUR SLIDER-']
                    )

                    # frame内にマスクを適用
                    # マスク処理部のみ.frameに変える
                    self.frame = cv2.bitwise_not(
                        cv2.bitwise_not(self.frame_roi),
                        self.frame,
                        mask=self.mask
                    )

                # フレーム数と経過秒数の表示
                cv2.putText(self.frame,
                            str("framecount: {0:.0f}".format(self.frame_count)),
                            (15,
                             20),
                            cv2.FONT_HERSHEY_SIMPLEX,
                            0.5,
                            (240,
                                230,
                                0),
                            1,
                            cv2.LINE_AA)
                cv2.putText(self.frame,
                            str("time: {0:.1f} sec".format(self.frame_count / self.fps)),
                            (15,
                                40),
                            cv2.FONT_HERSHEY_SIMPLEX,
                            0.5,
                            (240,
                                230,
                                0),
                            1,
                            cv2.LINE_AA)

                # 画像を表示
                cv2.imshow("Movie", self.frame)
                cv2.imshow("Mask", cv2.cvtColor(self.mask, cv2.COLOR_GRAY2BGR))

                if self.stop_flg:
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                else:
                    self.frame_count += 1
                    self.window['-PROGRESS SLIDER-'].update(
                        self.frame_count + 1)

                # その他の処理 ###############################################
                # ログウィンドウのクリア
                if self.event == 'Clear':
                    self.window['-OUTPUT-'].update('')

        finally:
            cv2.destroyWindow("Movie")
            cv2.destroyWindow("Mask")
            self.cap.release()
            self.window.close()


if __name__ == '__main__':
    Main().run()



```

# 参考リンク
[OpenCVとPythonで画像の自動補間（Fast Marching Method, Navier-Stokes）](https://qiita.com/hitomatagi/items/175c7f3a475cf463241b)
[Image Inpainting](https://docs.opencv.org/master/df/d3d/tutorial_py_inpainting.html)
[]()
[]()