はじめに
前回の記事に加えて、操作の対象として選ぶ領域ROI (Region of Interest)を付け加えます。
また、フレーム、ROIを切り出した画像を保存する機能を付け加えます。コーデックは種々選択可能ですが、圧縮率の高いDIVX, ImageJで解析可能なMJPG, Qiitaに張り付け可能なGIFで保存できるようにします。
前回記事:
PySimpleGUI + OpenCVで動画プレイヤーを作る
この記事でできること
動画のサイズをスライダで変更できます。
マウスで矩形選択した部分に、グレースケール、ぼかしを入れることが出来ます。
選択部分をDIVX, MJPEG, GIFのいずれかで保存することが出来ます。
動画の読込
前回と同様PySimpleGUIを使用してファイルの読込GUIを生成します。
class Main:
def __init__(self):
self.fp = file_read()
self.cap = cv2.VideoCapture(str(self.fp))
# 1フレーム目の取得
# 取得可能かの確認
self.ret, self.f_frame = self.cap.read()
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)
# フレーム関係
self.frame_count = 0
self.s_frame = 0
self.e_frame = self.total_count
# 再生の一時停止フラグ
self.stop_flg = False
cv2.namedWindow("Movie")
else:
sg.Popup("ファイルの読込に失敗しました。")
return
動画の読込
class Main:
def __init__(self):
self.fp = file_read()
self.cap = cv2.VideoCapture(str(self.fp))
# 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の初期サイズは動画サイズと同じにします。
# ROI
self.frames_roi = np.zeros((5, self.height, self.width))
# オリジナルサイズの保存
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.rec_flg = False
# マウスの動きの制御
# マウスのボタンが押されているかどうか
self.mouse_flg = False
self.event = ""
# ROIへの演算を適応するかどうか
self.roi_flg = True
cv2.namedWindow("Movie")
動画ウィンドウの左クリックDOWN → UP で矩形選択できるようコールバック関数を登録します。
# マウスイベントのコールバック登録
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
GUIの生成
ROIに実施する処理として、グレイスケール、ぼかしを追加しています。
画像サイズの変更スライダも追加しています。
def run(self):
# GUI #######################################################
# GUIのレイアウト
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
)
],
])
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)
)
],
])
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
動画の保存
動画ファイルとして保存する場合は、cv2.VideoWriter_fourccを使用します。ここでは、圧縮率が高いDIVXとフリーの動画解析ソフトImageJで読み込み可能なMJPEG形式で保存できるように設定しています。
GIFファイルで保存する場合は、Pillowを使用しています。
# 動画の書き出し
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 == '-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
画像処理の実施
以降に、サイズ変更、グレースケール化、ぼかしなどの処理を記述していきます。
フレーム全体に行う処理であるサイズ変更を行った後、ROIにのみグレースケール化とぼかしの処理を行っています。
# 以降にフレームに対する処理を記述 ##################################
# 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, :
]
# ぼかし
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はフレームに戻して表示させます。
# 処理した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)
)
# 画像を表示
cv2.imshow("Movie", self.frame)
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")
self.cap.release()
window.close()
if __name__ == '__main__':
Main().run()