Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roi plugin #1788

Merged
merged 34 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6a26cfd
Gitignore possible .venv directory
Jan 7, 2020
7efa3d2
Add basic Roi scaffold (WIP)
Jan 7, 2020
dc18230
Add working UI interaction to new Roi
Jan 8, 2020
f188c28
Correctly handle flipped image for clicking
Jan 9, 2020
4e9c54c
Add type hints and fix mypy warnings
Jan 9, 2020
3623e9f
Change Handles from IntEnum to simple Enum
Jan 9, 2020
90fd2fe
Add code documentation
Jan 9, 2020
5a7d7f9
Add license header
Jan 9, 2020
6ccc196
Fix bounds cutoff corner case
Jan 9, 2020
d61891f
Add RoiModel tests
Jan 9, 2020
95d736f
Don't invalidate model on missing frames
Jan 9, 2020
e2f62de
Apply bounds threshold when scaling frame_size
Jan 9, 2020
da3fd27
Fix correct positioning of the ROI outline
Jan 9, 2020
e46eedc
Use actual ROI outline color
Jan 9, 2020
0890dd2
Replace u_r usage with new roi
Jan 9, 2020
a1d1528
Hide RoiModel internal variables
Jan 9, 2020
71b2663
Add change callback to model for allowing g_pool modifications
Jan 9, 2020
37240e6
Change roi network format to the same as the internal representation
Jan 10, 2020
960b14d
Ensure roi always has an area >= 1
Jan 10, 2020
fc54f26
Fix active handle visualization not resetting correctly
Jan 10, 2020
93910be
Add return type
Jan 10, 2020
a37f0a1
Allow recovering the GUI for ROI with small areas
Jan 10, 2020
fd2ef44
Add get_init_dict to Roi
Jan 10, 2020
4267563
Remove old u_r code from eye.py
Jan 10, 2020
cf39167
Correctly handle resolution changes in detector_base_plugin
Jan 10, 2020
664adca
Remove old workaround for wrong processing order
Jan 10, 2020
d797bcd
Load Roi plugin at correct order
Jan 10, 2020
19032f7
Hide new roi interactivity when not in 'roi' display mode
Jan 10, 2020
f2085e0
Remove all old roi code
Jan 10, 2020
b259b94
Update and format tests
Jan 10, 2020
b96a14c
Fix typo found by tests
Jan 10, 2020
0976a36
Fix formatting issue in tests
Jan 10, 2020
4a956a0
Adjust type hints based on mypy feedback
Jan 10, 2020
22260fa
Make RoiModel an Observable
Jan 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ deployment/pupil_v*
*.pyd
*.dll
win_drv
.vs
.vs
.venv
88 changes: 19 additions & 69 deletions pupil_src/launchables/eye.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ def eye(
from gl_utils import make_coord_system_pixel_based
from gl_utils import make_coord_system_norm_based
from gl_utils import is_window_visible, glViewport
from ui_roi import UIRoi

# monitoring
import psutil
Expand All @@ -145,6 +144,7 @@ def eye(
from av_writer import JPEG_Writer, MPEG_Writer
from ndsi import H264Writer
from video_capture import source_classes, manager_classes
from roi import Roi

from background_helper import IPC_Logging_Task_Proxy
from pupil_detector_plugins import available_detector_plugins
Expand Down Expand Up @@ -175,7 +175,6 @@ def interrupt_handler(sig, frame):

icon_bar_width = 50
window_size = None
camera_render_size = None
hdpi_factor = 1.0

# g_pool holds variables for this process
Expand All @@ -188,6 +187,7 @@ def interrupt_handler(sig, frame):
g_pool.eye_id = eye_id
g_pool.process = f"eye{eye_id}"
g_pool.timebase = timebase
g_pool.camera_render_size = None

g_pool.ipc_pub = ipc_socket

Expand All @@ -202,7 +202,7 @@ def get_timestamp():
manager_classes
+ source_classes
+ available_detectors
+ [PupilDetectorManager]
+ [PupilDetectorManager, Roi]
)
g_pool.plugin_by_name = {p.__name__: p for p in plugins}

Expand All @@ -229,20 +229,20 @@ def get_timestamp():
# Detector needs to be loaded first to set `g_pool.pupil_detector`
(default_detector_cls.__name__, {}),
("PupilDetectorManager", {}),
("Roi", {}),
]

# Callback functions
def on_resize(window, w, h):
nonlocal window_size
nonlocal camera_render_size
nonlocal hdpi_factor

active_window = glfw.glfwGetCurrentContext()
glfw.glfwMakeContextCurrent(window)
hdpi_factor = glfw.getHDPIFactor(window)
g_pool.gui.scale = g_pool.gui_user_scale * hdpi_factor
window_size = w, h
camera_render_size = w - int(icon_bar_width * g_pool.gui.scale), h
g_pool.camera_render_size = w - int(icon_bar_width * g_pool.gui.scale), h
g_pool.gui.update_window(w, h)
g_pool.gui.collect_menus()
for g in g_pool.graphs:
Expand All @@ -264,16 +264,18 @@ def on_window_mouse_button(window, button, action, mods):
g_pool.gui.update_button(button, action, mods)

def on_pos(window, x, y):
x *= hdpi_factor
y *= hdpi_factor
x, y = x * hdpi_factor, y * hdpi_factor
g_pool.gui.update_mouse(x, y)

if g_pool.u_r.active_edit_pt:
pos = normalize((x, y), camera_render_size)
if g_pool.flip:
pos = 1 - pos[0], 1 - pos[1]
pos = denormalize(pos, g_pool.capture.frame_size)
g_pool.u_r.move_vertex(g_pool.u_r.active_pt_idx, pos)
pos = x, y
pos = normalize(pos, g_pool.camera_render_size)
if g_pool.flip:
pos = 1 - pos[0], 1 - pos[1]
# Position in img pixels
pos = denormalize(pos, g_pool.capture.frame_size)

for p in g_pool.plugins:
p.on_pos(pos)

def on_scroll(window, x, y):
g_pool.gui.update_scroll(x, y * scroll_factor)
Expand Down Expand Up @@ -371,32 +373,6 @@ def set_window_size():
f_width += int(icon_bar_width * g_pool.gui.scale)
glfw.glfwSetWindowSize(main_window, f_width, f_height)

def uroi_on_mouse_button(button, action, mods):
if g_pool.display_mode == "roi":
if action == glfw.GLFW_RELEASE and g_pool.u_r.active_edit_pt:
g_pool.u_r.active_edit_pt = False
# if the roi interacts we dont want
# the gui to interact as well
return
elif action == glfw.GLFW_PRESS:
x, y = glfw.glfwGetCursorPos(main_window)
# pos = normalize(pos, glfw.glfwGetWindowSize(main_window))
x *= hdpi_factor
y *= hdpi_factor
pos = normalize((x, y), camera_render_size)
if g_pool.flip:
pos = 1 - pos[0], 1 - pos[1]
# Position in img pixels
pos = denormalize(
pos, g_pool.capture.frame_size
) # Position in img pixels
if g_pool.u_r.mouse_over_edit_pt(
pos, g_pool.u_r.handle_size, g_pool.u_r.handle_size
):
# if the roi interacts we dont want
# the gui to interact as well
return

general_settings.append(ui.Button("Reset window size", set_window_size))
general_settings.append(ui.Switch("flip", g_pool, label="Flip image display"))
general_settings.append(
Expand Down Expand Up @@ -439,11 +415,6 @@ def uroi_on_mouse_button(button, action, mods):

g_pool.writer = None

g_pool.u_r = UIRoi((g_pool.capture.frame_size[1], g_pool.capture.frame_size[0]))
roi_user_settings = session_settings.get("roi")
if roi_user_settings and tuple(roi_user_settings[-1]) == g_pool.u_r.get()[-1]:
g_pool.u_r.set(roi_user_settings)

# Register callbacks main_window
glfw.glfwSetFramebufferSizeCallback(main_window, on_resize)
glfw.glfwSetWindowIconifyCallback(main_window, on_iconify)
Expand Down Expand Up @@ -563,19 +534,6 @@ def window_should_update():

frame = event.get("frame")
if frame:
f_width, f_height = g_pool.capture.frame_size
# TODO: Roi should be its own plugin. This way we could put it at the
# appropriate order for recent_events() to process frame resolution
# changes immediately after the backend.
if (g_pool.u_r.array_shape[0], g_pool.u_r.array_shape[1]) != (
f_height,
f_width,
):
g_pool.pupil_detector.on_resolution_change(
(g_pool.u_r.array_shape[1], g_pool.u_r.array_shape[0]),
g_pool.capture.frame_size,
)
g_pool.u_r = UIRoi((f_height, f_width))
if should_publish_frames:
try:
if frame_publish_format == "jpeg":
Expand Down Expand Up @@ -631,16 +589,10 @@ def window_should_update():
glfw.glfwMakeContextCurrent(main_window)
clear_gl_screen()

glViewport(0, 0, *camera_render_size)
glViewport(0, 0, *g_pool.camera_render_size)
for p in g_pool.plugins:
p.gl_display()

glViewport(0, 0, *camera_render_size)
# render the ROI
g_pool.u_r.draw(g_pool.gui.scale)
if g_pool.display_mode == "roi":
g_pool.u_r.draw_points(g_pool.gui.scale)

glViewport(0, 0, *window_size)
# render graphs
fps_graph.draw()
Expand All @@ -662,13 +614,12 @@ def window_should_update():
for button, action, mods in user_input.buttons:
x, y = glfw.glfwGetCursorPos(main_window)
pos = x * hdpi_factor, y * hdpi_factor
pos = normalize(pos, camera_render_size)
pos = normalize(pos, g_pool.camera_render_size)
if g_pool.flip:
pos = 1 - pos[0], 1 - pos[1]
# Position in img pixels
pos = denormalize(pos, g_pool.capture.frame_size)

# TODO: remove when ROI is plugin
uroi_on_mouse_button(button, action, mods)

for plugin in g_pool.plugins:
if plugin.on_click(pos, button, action):
break
Expand Down Expand Up @@ -698,7 +649,6 @@ def window_should_update():
session_settings["loaded_plugins"] = g_pool.plugins.get_initializers()
# save session persistent settings
session_settings["gui_scale"] = g_pool.gui_user_scale
session_settings["roi"] = g_pool.u_r.get()
session_settings["flip"] = g_pool.flip
session_settings["display_mode"] = g_pool.display_mode
session_settings["ui_config"] = g_pool.gui.configuration
Expand Down
48 changes: 0 additions & 48 deletions pupil_src/shared_modules/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,54 +54,6 @@ def get_dt():
yield dt


class Roi(object):
"""this is a simple 2D Region of Interest class
it is applied on numpy arrays for convenient slicing
like this:

roi_array_slice = full_array[r.view]
# do something with roi_array_slice

this creates a view, no data copying done
"""

def __init__(self, array_shape):
self.array_shape = array_shape
self.lX = 0
self.lY = 0
self.uX = array_shape[1]
self.uY = array_shape[0]
self.nX = 0
self.nY = 0

@property
def view(self):
return slice(self.lY, self.uY), slice(self.lX, self.uX)

def add_vector(self, vector):
"""
adds the roi offset to a len2 vector
"""
x, y = vector
return (self.lX + x, self.lY + y)

def sub_vector(self, vector):
"""
subs the roi offset to a len2 vector
"""
x, y = vector
return (x - self.lX, y - self.lY)

def set(self, vals):
if vals is not None and len(vals) is 5:
self.lX, self.lY, self.uX, self.uY, self.array_shape = vals
elif vals is not None and len(vals) is 4:
self.lX, self.lY, self.uX, self.uY = vals

def get(self):
return self.lX, self.lY, self.uX, self.uY, self.array_shape


def project_distort_pts(
pts_xyz,
camera_matrix,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,8 @@ def __init__(
self.proxy = PropertyProxy(self.detector_2d)

def detect(self, frame):
roi = Roi(*self.g_pool.u_r.get()[:4])
if (
not 0 <= roi.x_min <= roi.x_max < frame.width
or not 0 <= roi.y_min <= roi.y_max < frame.height
):
# TODO: Invalid ROIs can occur when switching camera resolutions, because we
# adjust the roi only after all plugin recent_events() have been called.
# Optimally we make a plugin out of the ROI and call its recent_events()
# immediately after the backend, before the detection.
logger.debug(f"Invalid Roi {roi} for img {frame.width}x{frame.height}!")
return None
# convert roi-plugin to detector roi
roi = Roi(*self.g_pool.roi.bounds)

debug_img = frame.bgr if self.g_pool.display_mode == "algorithm" else None
result = self.detector_2d.detect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,8 @@ def __init__(
self.debugVisualizer3D = Eye_Visualizer(g_pool, self.detector_3d.focal_length())

def detect(self, frame):
roi = Roi(*self.g_pool.u_r.get()[:4])
if (
not 0 <= roi.x_min <= roi.x_max < frame.width
or not 0 <= roi.y_min <= roi.y_max < frame.height
):
# TODO: Invalid ROIs can occur when switching camera resolutions, because we
# adjust the roi only after all plugin recent_events() have been called.
# Optimally we make a plugin out of the ROI and call its recent_events()
# immediately after the backend, before the detection.
logger.debug(f"Invalid Roi {roi} for img {frame.width}x{frame.height}!")
return None
# convert roi-plugin to detector roi
roi = Roi(*self.g_pool.roi.bounds)

debug_img = frame.bgr if self.g_pool.display_mode == "algorithm" else None
result = self.detector_3d.detect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,20 @@ def __init__(self, g_pool):
"pupil_detector.broadcast_properties": self.handle_broadcast_properties_notification,
"pupil_detector.set_property": self.handle_set_property_notification,
}
self._last_frame_size = None

def recent_events(self, event):
frame = event.get("frame")
if not frame:
self._recent_detection_result = None
return

frame_size = (frame.width, frame.height)
if frame_size != self._last_frame_size:
if self._last_frame_size is not None:
self.on_resolution_change(self._last_frame_size, frame_size)
self._last_frame_size = frame_size

detection_result = self.detect(frame=frame)
event["pupil_detection_result"] = detection_result
self._recent_detection_result = detection_result
Expand Down Expand Up @@ -105,27 +112,35 @@ def handle_set_property_notification(self, notification):
)
elif property_name == "roi":
# Modify the ROI with the values sent over network

try:
minX, maxX, minY, maxY = property_value
minX, minY, maxX, maxY = property_value
except (ValueError, TypeError) as err:
# NOTE: ValueError gets throws when length of the tuple does not
# match. TypeError gets thrown when it is not a tuple.
raise ValueError(
"ROI needs to be 4 integers: (minX, maxX, minY, maxY)"
"ROI needs to be 4 integers: (minX, minY, maxX, maxY)"
) from err
if minX > maxX or minY > maxY:
raise ValueError("ROI malformed: minX > maxX or minY > maxY!")
ui_roi = self.g_pool.u_r
ui_roi.lX = max(ui_roi.min_x, int(minX))
ui_roi.lY = max(ui_roi.min_y, int(minY))
ui_roi.uX = min(ui_roi.max_x, int(maxX))
ui_roi.uY = min(ui_roi.max_y, int(maxY))

# Apply very strict error checking here, although roi deal with invalid
# values, so the user gets immediate feedback and does not wonder why
# something did not work as expected.
width, height = self.g_pool.roi.frame_size
if not ((0 <= minX < maxX < width) and (0 <= minY < maxY <= height)):
raise ValueError(
"Received ROI with invalid dimensions!"
f" (minX={minX}, minY={minY}, maxX={maxX}, maxY={maxY})"
f" for frame size ({width} x {height})"
)

self.g_pool.roi.bounds = (minX, minY, maxX, maxY)

else:
raise KeyError(
"Notification subject does not "
"specifiy detector type nor modify ROI."
)
logger.debug(f"`{property_name}` property set to {property_value}")
logger.debug(f"'{property_name}' property set to {property_value}")
except KeyError:
logger.error("Malformed notification received")
logger.debug(traceback.format_exc())
Expand Down
Loading