In addition to the previous article, we will add a region of interest (ROI) to be selected as the operation target and a process (mask) that targets only a specific part.
Make a video player with PySimpleGUI + OpenCV 2 Add ROI setting and save function (DIVX, MJPG, GIF)
You can apply processing to a specific part (mask) in the ROI. The mask selection converts the image to HSV so that you can select the numerical value with each slider. The mask part is displayed in a separate window so that you can check it.
If you select red, you need to select two ranges of Hue values, 0-20 and 110-255, so create a checkbox called Hue Reverse, and if the checkbox is selected, The value outside the two sliders is set as the mask range.
The range between the left and right sliders of Hue is the mask range.
The outside of the left and right sliders of Hue is the mask range. The mask range can be inverted.
You can save a specified range of videos as DIVX, MJEG, GIF.
Select the file to load using the GUI.
import PySimpleGUI as sg
import cv2
import numpy as np
from PIL import Image
from pathlib import Path
def file_read():
'''
Select a file to read
'''
fp = ""
#GUI layout
layout = [
[
sg.FileBrowse(key="file"),
sg.Text("File"),
sg.InputText()
],
[sg.Submit(key="submit"), sg.Cancel("Exit")]
]
#WINDOW generation
window = sg.Window("File selection", layout)
#Event loop
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("No file has been entered.")
event = ""
else:
fp = values[0]
break
window.close()
return Path(fp)
HSV conversion → Mask processing is made into a function. Create a mask from the min and max values of H, S, and V received from the GUI, and create a mask image with cv2.bitwize_and ().
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))
#Video save flag
self.rec_flg = False
#Get the first frame
#Check if it can be obtained
self.ret, self.f_frame = self.cap.read()
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
#If you can get the frame, get various parameters
if self.ret:
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
#Acquisition of video information
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))
As a mask image, prepare a grayscale mask image of the same size as the ROI.
#Definition of mask image
self.mask = np.zeros_like(self.f_frame[:, :, 0])
#Save original size
self.org_width = self.width
self.org_height = self.height
#Frame related
self.frame_count = 0
self.s_frame = 0
self.e_frame = self.total_count
#Image cutout position
self.x1 = 0
self.y1 = 0
self.x2 = self.width
self.y2 = self.height
#Playback pause flag
self.stop_flg = False
#Control of mouse movement
#Whether the mouse button is pressed
self.mouse_flg = False
self.event = ""
#Whether to apply operations to ROI
self.roi_flg = True
cv2.namedWindow("Movie")
#Mouse event callback registration
cv2.setMouseCallback("Movie", self.onMouse)
#Exit if the frame could not be obtained
else:
sg.Popup("Failed to read the file.")
return
#Mouse event
def onMouse(self, event, x, y, flags, param):
#Left click
if event == cv2.EVENT_LBUTTONDOWN:
self.x1 = self.x2 = x
self.y1 = self.y2 = y
#Start drawing a rectangle. Press the mouse once to start drawing a rectangle.
self.mouse_flg = True
#Pause the calculation of the ROI part
self.roi_flg = False
return
elif event == cv2.EVENT_LBUTTONUP:
#Stop updating rectangles
self.mouse_flg = False
#Start calculation on ROI
self.roi_flg = True
#If the ROI selection is 0, reset it and stop the ROI calculation.
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 <to be 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
#Show ROI range
print(
"ROI x:{0}:{1} y:{2}:{3}".format(
str(self.x1),
str(self.x2),
str(self.y1),
str(self.y2)
)
)
return
#Continues to display rectangle when mouse is pressed
if self.mouse_flg:
self.x2 = x
self.y2 = y
return
def run(self):
# GUI #######################################################
#GUI layout
#Tab 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
)
],
])
Prepare a tab for mask processing. When Masking is selected with the radio button Masking is performed.
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')]
]
#Generate Window
window = sg.Window('OpenCV Integration', layout, location=(0, 0))
#Display of video information
self.event, values = window.read(timeout=0)
print("The file has been read.")
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)))
#Main loop#########################################################
try:
while True:
#Loading GUI events
self.event, values = window.read(
timeout=values["-SPEED SLIDER-"]
)
#Show event in window
if self.event != "__TIMEOUT__":
print(self.event)
#Exit when the Exit button is pressed or when the window close button is pressed
if self.event in ('Exit', sg.WIN_CLOSED, None):
break
#Reload video
#Works when the start frame is set
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
#Continue to reflect changes to Progress slider
continue
#Export video
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:
#Save as video
#Codec selection
#DIVX has a high compression rate
#MJEG can be analyzed with 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
#Frame operation################################################
#Priority is given if the slider is changed directly
if self.event == '-PROGRESS SLIDER-':
#Set the frame count to the progress bar
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 you change the start frame
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 you change the end 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
#End frame settings
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 the counter exceeds the end frame, restart from the start frame
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
#Pause video loading with the stop button
if self.event == 'Play / Stop':
self.stop_flg = not self.stop_flg
#Unless the stop flag is set and an event occurs, count in
#Stop the operation
#If the stop button is pressed, the video processing will be stopped, but something
#If an event occurs, only update the image
#The same applies when operating the mouse
if(
(
self.stop_flg
and self.event == "__TIMEOUT__"
and self.mouse_flg is False
)
):
window['-PROGRESS SLIDER-'].update(self.frame_count)
continue
#Skip frames
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)
#Load frame##############################################
self.ret, self.frame = self.cap.read()
self.valid_frame = int(self.frame_count - self.s_frame)
#Self when the last frame is over.s_Resume from frame
if not self.ret:
self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.s_frame)
self.frame_count = self.s_frame
continue
#Describe the processing for the frame after that##################################
#First carry out processing for the entire frame##############################
#resize
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
#Perform processing for ROI##########################################
if self.roi_flg:
self.frame_roi = self.frame[
self.y1:self.y2, self.x1:self.x2, :
]
Describe the mask processing. The color is specified in RGB instead of HSV. After specifying the color in RGB, you can specify the mask range in V (brightness) of HSV. In this case, Hue should specify the entire range 0-255 and change only V.
#Processing to MASK images##################################################################
if values['-MASKING-']:
#RGB separation
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
Describes mask processing in HSV. The Red, Green, and Blue buttons allow you to select each color within a certain range. You can mask the object by adjusting the threshold while watching the original video and the mask video.
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-']
)
#Grayscale
self.mask = cv2.cvtColor(
self.mask,
cv2.COLOR_BGR2GRAY
)
#Blur
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
)
Apply processing only to the mask range within the ROI. I'm using cv2.bitwise_not nested, but there may be a better way to do this.
if values['-MASKING-']:
# frame_Apply mask inside roi
#Only the mask processing part is framed_Change to 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
)
#Return processed ROI to frame
self.frame[self.y1:self.y2, self.x1:self.x2, :] = self.frame_roi
#Save video
if self.rec_flg:
#Cut out roi again after image stabilization
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)
#Display during saving
cv2.putText(
self.frame,
str("Now Recording"),
(20, 60),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(10, 10, 255),
1,
cv2.LINE_AA
)
# e_Finish when it becomes a 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
#Display of number of frames and elapsed seconds
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
)
#When performing calculation to ROI or while pressing the left mouse button
#Draw a rectangle
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)
)
The mask image is displayed only when Masking is selected.
#Display image
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)
#Other processing###############################################
#Clear log window
if self.event == 'Clear':
window['-OUTPUT-'].update('')
finally:
cv2.destroyWindow("Movie")
cv2.destroyWindow("Mask")
self.cap.release()
window.close()
if __name__ == '__main__':
Main().run()
Let's recognize red objects with python Wikipedia: HSV color space Alpha blending and masking images with Python, OpenCV, NumPy
Recommended Posts