PySimpleGUI + OpenCVで動画プレイヤーを作る3 マスク機能の追加 

# はじめに
前回の記事に加えて、操作の対象として選ぶ領域ROI (Region of Interest)と特定の部分のみを処理対象とする処理（マスク）を付け加えます。

[PySimpleGUI + OpenCVで動画プレイヤーを作る2 ROI設定と保存機能の追加(DIVX, MJPG, GIF)](https://qiita.com/_yane/items/8ffd4e3e8ecbc1af9ac5)

# この記事でできること
ROI内の特定の部分(マスク)に処理を掛けることが出来ます。
マスクの選択は画像をHSVに変換し、それぞれスライダで数値を選択できるようにしています。マスク部分は別ウィンドウで表示して確認できるようにしています。
![3-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205544/5cd04611-9835-b4a2-c2ec-4386697389a9.jpeg)

赤色を選択する場合は、Hueの値を0-20と110-255程度の二つの範囲を選択する必要があるため、Hue Reverseというチェックボックスを作成し、チェックボックスが選択されている場合は、2つのスライダの外側の値をマスク範囲とするようにしています。
#### Hue Reverse　【オフ】
Hueの左右のスライダの間の範囲がマスク範囲となります。
![クリップボード一時ファイル01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205544/cd4657ea-2114-12e2-4c04-2eabb533b2d2.jpeg)

#### Hue Reverse　【オン】
Hueの左右のスライダの外側がマスク範囲となります。マスク範囲を反転することが出来ます。
![クリップボード一時ファイル02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205544/25fd978a-96f8-42e4-c71c-96dbeba3996a.jpeg)

指定範囲の動画をDIVX, MJEG, GIFで保存することが出来ます。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205544/62a9ec7e-718a-0e51-6da1-917b4c18e042.gif)

## ファイルの読込
GUIを利用して読み込むファイルを選択します。
```python
import PySimpleGUI as sg
import cv2
import numpy as np
from PIL import Image
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")]
    ]
    # 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変換 → マスク処理を関数化しています。GUIから受け取ったH, S, Vそれぞれのmin, maxの値からmaskを作成し、cv2.bitwize_and()でマスク画像を作成します。

```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


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.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            self.total_count = self.cap.get(cv2.CAP_PROP_FRAME_COUNT)

            # ROI
            self.frames_roi = np.zeros((5, self.height, self.width))
```
マスク画像として、ROIと同サイズのグレースケールのマスク画像を用意しておきます。

```python


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

            # オリジナルサイズの保存
            self.org_width = self.width
            self.org_height = self.height

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

            # 画像きり抜き位置
            self.x1 = 0
            self.y1 = 0
            self.x2 = self.width
            self.y2 = self.height

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

            # マウスの動きの制御
            # マウスのボタンが押されているかどうか
            self.mouse_flg = False
            self.event = ""
            # ROIへの演算を適応するかどうか
            self.roi_flg = True
            cv2.namedWindow("Movie")

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

    # マウスイベント
    def onMouse(self, event, x, y, flags, param):
        # 左クリック
        if event == cv2.EVENT_LBUTTONDOWN:
            self.x1 = self.x2 = x
            self.y1 = self.y2 = y
            # 長方形の描写開始。マウスを一回押すと長方形描写を開始する。
            self.mouse_flg = True
            # ROI部分の演算を一時停止
            self.roi_flg = False
            return

        elif event == cv2.EVENT_LBUTTONUP:
            # 長方形の更新を停止
            self.mouse_flg = False
            # ROIへの演算を開始する
            self.roi_flg = True
            # ROIの選択で0の場合はリセットし、ROIの演算をストップ
            if (
                x == self.x1
                or y == self.y1
                or x <= 0
                or y <= 0
            ):
                self.x1 = 0
                self.y1 = 0
                self.x2 = self.width
                self.y2 = self.height
                return

            # x1 < x2になるようにする
            elif self.x1 < x:
                self.x2 = x
            else:
                self.x2 = self.x1
                self.x1 = x

            if self.y1 < y:
                self.y2 = y
            else:
                self.y2 = self.y1
                self.y1 = y

            # ROI範囲を表示
            print(
                "ROI x:{0}:{1}  y:{2}:{3}".format(
                    str(self.x1),
                    str(self.x2),
                    str(self.y1),
                    str(self.y2)
                )
            )
            return

        # マウスが押下されている場合、長方形を表示し続ける
        if self.mouse_flg:
            self.x2 = x
            self.y2 = y

            return

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

        # タブ1
        T1 = sg.Tab("Basic", [
            [
                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(
                    '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
                )
            ],


        ])

        T2 = sg.Tab("processing", [
            [
                sg.Checkbox(
                    'gray',
                    size=(10, 1),
                    key='-GRAY-',
                    enable_events=True
                )
            ],
        ])
```

## マスク設定用GUI
マスク処理用のタブを用意します。
ラジオボタンでMaskingを選択した場合マスク処理を行います。

```python
        T3 = sg.Tab("mask", [
            [
                sg.Radio(
                    'Rectangle',
                    "RADIO2",
                    key='-RECTANGLE_MASK-',
                    default=True,
                    size=(8, 1)
                ),
                sg.Radio(
                    'Masking',
                    "RADIO2",
                    key='-MASKING-',
                    size=(8, 1)
                )
            ],
            [
                sg.Checkbox(
                    "Blue",
                    size=(10, 1),
                    default=True,
                    key='-BLUE_MASK-',
                    enable_events=True
                ),
                sg.Checkbox(
                    "Green",
                    size=(10, 1),
                    default=True,
                    key='-GREEN_MASK-',
                    enable_events=True
                ),
                sg.Checkbox(
                    "Red",
                    size=(10, 1),
                    default=True,
                    key='-RED_MASK-',
                    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
                )
            ]
        ])
        T4 = sg.Tab("Save", [
            [
                sg.Button('Write', size=(10, 1)),
                sg.Radio(
                    'DIVX',
                    "RADIO1",
                    key='-DIVX-',
                    default=True,
                    size=(8, 1)
                ),
                sg.Radio('MJPG', "RADIO1", key='-MJPG-', size=(8, 1)),
                sg.Radio('GIF', "RADIO1", key='-GIF-', size=(8, 1))
            ],
            [
                sg.Text('Caption', size=(10, 1)),
                sg.InputText(
                    size=(32, 50),
                    key='-CAPTION-',
                    enable_events=True
                )
            ]
        ])

        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.Text("End ", size=(8, 1)),
                sg.Slider(
                    (0, self.total_count - 1), self.total_count - 1,
                    1,
                    orientation='h',
                    size=(45, 15),
                    key='-END 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.Button('<<<', size=(5, 1)),
                sg.Button('<<', size=(5, 1)),
                sg.Button('<', size=(5, 1)),
                sg.Button('Play / Stop', size=(9, 1)),
                sg.Button('Reset', size=(7, 1)),
                sg.Button('>', size=(5, 1)),
                sg.Button('>>', size=(5, 1)),
                sg.Button('>>>', size=(5, 1))
            ],
            [
                sg.Text("Speed", size=(6, 1)),
                sg.Slider(
                    (0, 240),
                    10,
                    10,
                    orientation='h',
                    size=(19.4, 15),
                    key='-SPEED SLIDER-',
                    enable_events=True
                ),
                sg.Text("Skip", size=(6, 1)),
                sg.Slider(
                    (0, 300),
                    0,
                    1,
                    orientation='h',
                    size=(19.4, 15),
                    key='-SKIP SLIDER-',
                    enable_events=True
                )
            ],
            [sg.HorizontalSeparator()],
            [
                sg.TabGroup(
                    [[T1, T2, T3, T4]],
                    tab_background_color="#ccc",
                    selected_title_color="#fff",
                    selected_background_color="#444",
                    tab_location="topleft"
                )
            ],
            [sg.Output(size=(65, 5), key='-OUTPUT-')],
            [sg.Button('Clear')]
        ]

        # Windowを生成
        window = sg.Window('OpenCV Integration', layout, location=(0, 0))
        # 動画情報の表示
        self.event, values = 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 = window.read(
                    timeout=values["-SPEED SLIDER-"]
                )

                # イベントをウィンドウに表示
                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
                    window['-PROGRESS SLIDER-'].update(self.frame_count)

                    self.video_stabilization_flg = False
                    self.stab_prepare_flg = False

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

                # 動画の書き出し
                if self.event == 'Write':
                    self.rec_flg = True
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
                    self.frame_count = self.s_frame
                    window['-PROGRESS SLIDER-'].update(self.frame_count)

                    if values["-GIF-"]:
                        images = []
                    else:
                        # 動画として保存
                        # コーデックの選択
                        # DIVXは圧縮率高い
                        # MJEGはImageJで解析可能
                        if values["-DIVX-"]:
                            codec = "DIVX"
                        elif values["-MJPG-"]:
                            codec = "MJPG"
                        fourcc = cv2.VideoWriter_fourcc(*codec)
                        out = cv2.VideoWriter(
                            str((
                                self.fp.parent / (self.fp.stem + '_' + codec + '.avi')
                            )),
                            fourcc,
                            self.fps,
                            (int(self.x2 - self.x1), int(self.y2 - self.y1))
                        )
                    continue

                if self.event == 'Stabilization':
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
                    self.frame_count = self.s_frame
                    window['-PROGRESS SLIDER-'].update(self.frame_count)

                    self.play_count = int(self.e_frame - self.s_frame)
                    self.video_stabilization_flg = True
                    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 values['-PROGRESS SLIDER-'] > values['-END FRAME SLIDER-']:
                        window['-END FRAME SLIDER-'].update(
                            values['-PROGRESS SLIDER-'])

                # スタートフレームを変更した場合
                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
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    if values['-START FRAME SLIDER-'] > values['-END FRAME SLIDER-']:
                        window['-END FRAME SLIDER-'].update(
                            values['-START FRAME SLIDER-'])
                        self.e_frame = self.s_frame

                # エンドフレームを変更した場合
                if self.event == '-END FRAME SLIDER-':
                    if values['-END FRAME SLIDER-'] < values['-START FRAME SLIDER-']:
                        window['-START FRAME SLIDER-'].update(
                            values['-END FRAME SLIDER-'])
                        self.s_frame = self.e_frame

                    # エンドフレームの設定
                    self.e_frame = int(values['-END FRAME SLIDER-'])

                if self.event == '<<<':
                    self.frame_count = np.maximum(0, self.frame_count - 150)
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                if self.event == '<<':
                    self.frame_count = np.maximum(0, self.frame_count - 30)
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                if self.event == '<':
                    self.frame_count = np.maximum(0, self.frame_count - 1)
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                if self.event == '>':
                    self.frame_count = self.frame_count + 1
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                if self.event == '>>':
                    self.frame_count = self.frame_count + 30
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                if self.event == '>>>':
                    self.frame_count = self.frame_count + 150
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, 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
                    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__"
                        and self.mouse_flg is False
                    )
                ):
                    window['-PROGRESS SLIDER-'].update(self.frame_count)
                    continue

                # スキップフレーム分とばす
                if not self.stop_flg and values['-SKIP SLIDER-'] != 0:
                    self.frame_count += values["-SKIP SLIDER-"]
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.frame_count)

                # フレームの読込 ##############################################
                self.ret, self.frame = self.cap.read()
                self.valid_frame = int(self.frame_count - self.s_frame)
                # 最後のフレームが終わった場合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

                # 以降にフレームに対する処理を記述 ##################################
                # frame全体に対する処理をはじめに実施 ##############################
                # リサイズ
                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))
                if self.event == '-RESIZE SLIDER-':
                    self.x1 = self.y1 = 0
                    self.x2 = self.width
                    self.y2 = self.height
                # ROIに対して処理を実施 ##########################################
                if self.roi_flg:
                    self.frame_roi = self.frame[
                        self.y1:self.y2, self.x1:self.x2, :
                    ]
```

## RGBマスク処理
マスク処理を記述します。
HSVではなく、RGBでの色指定を入れています。RGBで色を指定した後、HSVのV(明度)でマスク範囲を指定することが出来ます。この場合は、Hueは0-255の全範囲を指定して、Vのみ変更したほうが良いです。

```python
                    # MASK画像への処理 ##################################################################
                    if values['-MASKING-']:
                        # RGB分離
                        self.mask = np.copy(self.frame_roi)
                        if not values['-BLUE_MASK-']:
                            self.mask[:, :, 0] = 0
                        if not values['-GREEN_MASK-']:
                            self.mask[:, :, 1] = 0
                        if not values['-RED_MASK-']:
                            self.mask[:, :, 2] = 0

```

## HSVマスク処理
HSVでのマスク処理を記述します。
Red, Green, Blueボタンで各色をある程度適当な範囲で選択できるようにしています。元動画とマスク動画を見ながら、閾値を調整して対象物をマスクすることが出来ます。

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

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

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

                        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-']
                        )

                        # グレイスケール
                        self.mask = cv2.cvtColor(
                            self.mask,
                            cv2.COLOR_BGR2GRAY
                        )
                    # ぼかし
                    if values['-BLUR-']:
                        self.frame_roi = cv2.GaussianBlur(
                            self.frame_roi, (21, 21), values['-BLUR SLIDER-']
                        )
                    if values['-GRAY-']:
                        self.frame_roi = cv2.cvtColor(
                            self.frame_roi,
                            cv2.COLOR_BGR2GRAY
                        )
                        self.frame_roi = cv2.cvtColor(
                            self.frame_roi,
                            cv2.COLOR_GRAY2BGR
                        )
```
ROI内のマスク範囲のみ処理を適用します。
cv2.bitwise_notを入れ子で使用して実施していますが、より適切な実現方法あるかもしれません。


```python
                    if values['-MASKING-']:
                        # frame_roi内にマスクを適用
                        # マスク処理部のみをframe_roiに変える
                        self.frame_roi = cv2.bitwise_not(
                            cv2.bitwise_not(self.frame_roi),
                            self.frame[self.y1:self.y2, self.x1:self.x2, :],
                            mask=self.mask
                        )
                    # 処理したROIをframeに戻す
                    self.frame[self.y1:self.y2, self.x1:self.x2, :] = self.frame_roi
                # 動画の保存
                if self.rec_flg:
                    # 手振れ補正後再度roiを切り抜き
                    self.frame_roi = self.frame[
                        self.y1:self.y2, self.x1:self.x2, :
                    ]
                    if values["-GIF-"]:
                        images.append(
                            Image.fromarray(
                                cv2.cvtColor(
                                    self.frame_roi, cv2.COLOR_BGR2RGB
                                )
                            )
                        )
                    else:
                        out.write(self.frame_roi)

                    # 保存中の表示
                    cv2.putText(
                        self.frame,
                        str("Now Recording"),
                        (20, 60),
                        cv2.FONT_HERSHEY_SIMPLEX,
                        0.5,
                        (10, 10, 255),
                        1,
                        cv2.LINE_AA
                    )

                    # e_frameになったら終了
                    if self.frame_count >= self.e_frame - values["-SKIP SLIDER-"] - 1:
                        if values["-GIF-"]:
                            images[0].save(
                                str((self.fp.parent / (self.fp.stem + '.gif'))),
                                save_all=True,
                                append_images=images[1:],
                                optimize=False,
                                duration=1000 // self.fps,
                                loop=0
                            )
                        else:
                            out.release()
                        self.rec_flg = False

                # フレーム数と経過秒数の表示
                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
                )

                # ROIへの演算を実施している場合 or マウス左ボタンを押している最中
                # 長方形を描写する
                if self.roi_flg or self.mouse_flg:
                    cv2.rectangle(
                        self.frame,
                        (self.x1, self.y1),
                        (self.x2 - 1, self.y2 - 1),
                        (128, 128, 128)
                    )
```
Maskingを選択している場合のみ、マスク画像を表示するようにしています。

```python
                # 画像を表示
                cv2.imshow("Movie", self.frame)
                if values['-MASKING-']:
                    cv2.imshow("Mask", cv2.cvtColor(self.mask, cv2.COLOR_GRAY2BGR))
                    cv2.setWindowProperty("Mask", cv2.WND_PROP_VISIBLE, 0)
                elif not values['-MASKING-'] and cv2.getWindowProperty("Mask", cv2.WND_PROP_VISIBLE):
                    cv2.destroyWindow("Mask")

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

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

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

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


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

```

# 参考リンク
[pythonで赤い物体を認識しよう](https://qiita.com/odaman68000/items/ae28cf7bdaf4fa13a65b)
[Wikipedia:　HSV色空間](https://ja.wikipedia.org/wiki/HSV%E8%89%B2%E7%A9%BA%E9%96%93)
[Python, OpenCV, NumPyで画像のアルファブレンドとマスク処理](https://note.nkmk.me/python-opencv-numpy-alpha-blend-mask/)
[]()
[]()
[]()
[]()