diff --git a/displayarray/__init__.py b/displayarray/__init__.py index 67889a8..15700a7 100644 --- a/displayarray/__init__.py +++ b/displayarray/__init__.py @@ -4,7 +4,7 @@ Display any array, webcam, or video file. display is a function that displays these in their own windows. """ -__version__ = "1.3.1" +__version__ = "2.0.0" from .window.subscriber_windows import display, breakpoint_display, read_updates, publish_updates from . import effects diff --git a/displayarray/frame/frame_publishing.py b/displayarray/frame/frame_publishing.py index f5573b3..122e9eb 100644 --- a/displayarray/frame/frame_publishing.py +++ b/displayarray/frame/frame_publishing.py @@ -18,10 +18,12 @@ try: using_pyv4l2cam = True except ImportError: - warnings.warn("Could not import PyV4L2Cam on linux. Camera capture will be slow.") - warnings.warn( - "To install, run: pip install git+https://github.com/simleek/PyV4L2Cam.git" - ) + pass + # while this is still good for raspberry pi, OpenCV tends to be faster for normal computers. + #warnings.warn("Could not import PyV4L2Cam on linux. Camera capture will be slow.") + #warnings.warn( + # "To install, run: pip install git+https://github.com/simleek/PyV4L2Cam.git" + #) import numpy as np diff --git a/displayarray/window/mglwindow.py b/displayarray/window/mglwindow.py new file mode 100644 index 0000000..a28daca --- /dev/null +++ b/displayarray/window/mglwindow.py @@ -0,0 +1,369 @@ +import numpy as np +import moderngl_window as mgw +import moderngl as mgl +import cv2 +import struct +from moderngl_window import geometry +import os + +dir_path = os.path.dirname(os.path.realpath(__file__)) + + +class MglWindowConfig(mgw.WindowConfig): + resizable = True + gl_version = (4, 3) + def __init__(self, **kwargs): + super().__init__(**kwargs) + mgw.window() + self.uibo = None + self.hit_buff = None + self.rbuf = None + self.last_frame = -1 + + '''def key_event(self, key, action, modifiers): + """Events for one time key presses, like pause""" + if key == 256: # pygame.K_ESCAPE + self.capturing_mouse = not self.capturing_mouse''' + + def set_in_buff(self, in_buff: 'UserInputUBO'): + self.uibo = in_buff + + def set_hit_buff(self, hit_buff: 'UserOutputUBO'): + self.hit_buff = hit_buff + def set_rect_buff(self, rect_buff: 'InputTextureInfosUBO'): + self.rbuf = rect_buff + def mouse_position_event(self, x, y, dx, dy): + if self.uibo is not None: + self.uibo.iMouse[0] = float(x) + self.uibo.iMouse[1] = float(y) + + def mouse_drag_event(self, x: int, y: int, dx: int, dy: int): + if self.hit_buff is not None: + frame = self.hit_buff.hit_level + if frame!=-1: + self.last_frame = frame + rect = self.rbuf.tex_levels[self.last_frame]['rect'] + rect[0] += dx + rect[1] += dy + rect[2] += dx + rect[3] += dy + + + + + +def create_no_input_texture(width=100, height=100): + # Create a black image + img = np.zeros((height, width, 3), np.uint8) + + # Write "no input" text in the middle + font = cv2.FONT_HERSHEY_SIMPLEX + text = "No Input" + text_size = cv2.getTextSize(text, font, 1, 2)[0] + text_x = (width - text_size[0]) // 2 + text_y = (height + text_size[1]) // 2 + cv2.putText(img, text, (text_x, text_y), font, 1, (255, 255, 255), 2, cv2.LINE_AA) + + return img + + +class InputTextureInfosUBO(object): + def __init__(self, start_textures=[]): + self.channels = 3 # Assuming RGB format + self.tex_levels = [] + self.input_image: np.ndarray = np.asarray([], dtype=np.float32) + self.no_input = not bool(start_textures) + + # Initialize input image buffer with a default "no input" image + if not start_textures: + start_textures = [create_no_input_texture()] + + # Initialize texture levels with default values + for s in start_textures: + tex_level = {'startIdx': 0, 'width': s.shape[0], 'height': s.shape[1], 'rect': [1.0, 0.0, 0.0, 1.0]} + self.tex_levels.append(tex_level) + self.input_image = np.concatenate((self.input_image, s.flatten()), axis=0, dtype=self.input_image.dtype) + + def append_input_stream(self, img:np.ndarray): + i = len(self.tex_levels) + start_index = self.tex_levels[-1]['startIdx']+\ + self.tex_levels[-1]['width']*self.tex_levels[-1]['height']*self.channels + width = img.shape[0] + height = img.shape[1] + assert img.shape[2] == self.channels + rect = [0,0,width,height] + self.tex_levels.append({ + 'startIdx': start_index, + 'width': width, + 'height': height, + 'rect': rect + }) + self.input_image = np.concatenate((self.input_image, img.flatten()), axis=0, dtype=self.input_image.dtype) + #self.input_image[start_index:] = img.flatten() + + return i + + def set_input_stream(self, i, img:np.ndarray): + start_index = self.tex_levels[i]['startIdx'] + end_index = start_index + img.shape[0] * img.shape[1] * self.channels + assert img.shape[2] == self.channels + if i!=len(self.tex_levels) and \ + self.tex_levels[i]['width']*self.tex_levels[i]['height']!=img.shape[0]*img.shape[1]: + ind = start_index + for j in range(i, len(self.tex_levels)): + new_start_index = ind + img.shape[0]*img.shape[1]*self.channels + self.tex_levels[j]['startIdx'] = new_start_index + ind = new_start_index + + self.tex_levels[i]['width'] = img.shape[0] + self.tex_levels[i]['height'] = img.shape[1] + + self.input_image[start_index:end_index] = img.flatten() + + def get_tex_data_buffer(self): + tex_data_bytes = bytearray() + tex_data_bytes.extend(struct.pack("<2ixxxxxxxx", self.channels, len(self.tex_levels))) + for level in self.tex_levels: + tex_data_bytes.extend(struct.pack("<3ixxxx", level['startIdx'], level['width'], level['height'])) + tex_data_bytes.extend(struct.pack("<4f", *level['rect'])) + return bytes(tex_data_bytes) + + def set_tex_data_buffer(self, data): + if len(data) - 2 % 7 != 0: + raise ValueError("Input data size does not match buffer format") + self.channels = data[0] + num_levels = data[1] + for i in range(num_levels): + self.tex_levels[i]['startIdx'] = data[i * 7 + 2] + self.tex_levels[i]['width'] = data[i * 7 + 3] + self.tex_levels[i]['height'] = data[i * 7 + 4] + self.tex_levels[i]['rect'] = data[i * 7 + 5:i * 7 + 9] + + def append_tex_data_buffer(self, data): + if len(data) != 7: + raise ValueError("Input data size does not match buffer format") + self.tex_levels.append({ + 'startIdx': data[0], + 'width': data[0], + 'height': data[0], + 'rect': data[0] + }) + + def get_tex_level_rect(self, level_idx): + return self.tex_levels[level_idx]['rect'] + + def set_tex_level_rect(self, level_idx, rect): + if len(rect) != 4: + raise ValueError("Rect must contain 4 values (vec4)") + self.tex_levels[level_idx]['rect'] = rect + + def get_input_image_buffer(self): + return self.input_image.tobytes() + + def set_input_image_buffer(self, data: np.ndarray): + if len(data) != len(self.input_image): + raise ValueError("Input data size does not match buffer size") + self.no_input = False + self.input_image = data.flatten() + + def append_input_image_buffer(self, data: np.ndarray): + if len(data) != len(self.input_image): + raise ValueError("Input data size does not match buffer size") + if self.no_input: + self.input_image = data.flatten() + self.no_input = False + else: + self.input_image = np.concatenate((self.input_image, data.flatten()), axis=0, dtype=self.input_image.dtype) + + +class UserInputUBO: + def __init__(self): + self.iMouse = np.zeros((2,), np.float32) + + def to_bytes(self): + return struct.pack(f"=texLevels[i].rect.x && + gl_FragCoord.y>=texLevels[i].rect.y && + gl_FragCoord.x 1) { + out_color.y = bilinearInterpolation( + fract(x_current), + fract(y_current), + inputImage[bottomLeftIdx+1], + inputImage[bottomRightIdx+1], + inputImage[topLeftIdx+1], + inputImage[topRightIdx+1] + ); + } + if(channels>2){ + out_color.z = bilinearInterpolation( + fract(x_current), + fract(y_current), + inputImage[bottomLeftIdx+2], + inputImage[bottomRightIdx+2], + inputImage[topLeftIdx+2], + inputImage[topRightIdx+2] + ); + } + // currently only supporting 3 channels at most. + }else{ + // nice white background. ( ∩´ ᐜ `∩) + out_color = vec4(1.0, 1.0, 1.0, 1.0); + } + + if(distance(iMouse, gl_FragCoord.xy)==0){ + hit_level = our_level; + //hit_pos = vec2(x_current, y_current); + hit_pos = iMouse; + } + +} \ No newline at end of file diff --git a/displayarray/window/subscriber_windows.py b/displayarray/window/subscriber_windows.py index 8128711..59eeee9 100644 --- a/displayarray/window/subscriber_windows.py +++ b/displayarray/window/subscriber_windows.py @@ -35,6 +35,7 @@ try: except: warnings.warn("Could not import ZMQ and tensorcom. Cannot send messages between programs.") +from . import mglwindow class SubscriberWindows(object): """Windows that subscribe to updates to cameras, videos, and arrays.""" @@ -61,6 +62,7 @@ class SubscriberWindows(object): self.ctx = None self.sock_list: List[zmq.Socket] = [] self.top_list: List[bytes] = [] + self.displayer = mglwindow.MglWindow() if callbacks is None: callbacks = [] @@ -75,10 +77,10 @@ class SubscriberWindows(object): def __bool__(self): self.update() - return not self.exited + return not self.exited and not self.displayer.window.is_closing def __iter__(self): - while not self.exited: + while not self.exited and not self.displayer.window.is_closing: self.update() yield self.frames @@ -93,15 +95,15 @@ class SubscriberWindows(object): uid = uid_for_source(name) self.source_names.append(uid) self.input_vid_global_names.append(uid) - self.input_cams.append(name) + self.input_cams.append(uid) return self def add_window(self, name): """Add another window for this class to display sources with. The name will be the title.""" self.window_names.append(name) - cv2.namedWindow(name + " (press ESC to quit)") - m = WeakMethod(self.handle_mouse) - cv2.setMouseCallback(name + " (press ESC to quit)", m) + #cv2.namedWindow(name + " (press ESC to quit)") + #m = WeakMethod(self.handle_mouse) + #cv2.setMouseCallback(name + " (press ESC to quit)", m) return self def add_callback(self, callback): @@ -160,7 +162,7 @@ class SubscriberWindows(object): else: if len(self.window_names) <= win_num: self.add_window(f"{prepend_name}{win_num}") - cv2.imshow( + self.displayer.imshow( self.window_names[win_num] + " (press ESC to quit)", f[i] ) win_num += 1 @@ -179,7 +181,7 @@ class SubscriberWindows(object): else: if len(self.window_names) <= win_num: self.add_window(f"{prepend_name} {win_num}") - cv2.imshow( + self.displayer.imshow( self.window_names[win_num] + " (press ESC to quit)", frames[f] ) win_num += 1 @@ -233,6 +235,7 @@ class SubscriberWindows(object): self.__check_too_many_channels() if not self.silent: self.display_frames(self.frames) + self.displayer.update() def update(self, arr: Union[List[np.ndarray], np.ndarray] = None, id: Union[List[str],str, List[int], int, None] = None): """Update window frames once. Optionally add a new input and input id.""" @@ -292,6 +295,7 @@ class SubscriberWindows(object): self.__stop_all_cams() for t in self.close_threads: t.join() + self.displayer.window.close() def __enter__(self): return self @@ -310,7 +314,7 @@ class SubscriberWindows(object): sub_cmd = window_commands.win_cmd_sub() msg_cmd = "" key = "" - while msg_cmd != "quit" and key != "quit": + while msg_cmd != "quit" and key != "quit" and (not self.displayer.window.is_closing): msg_cmd, key = self.update() sub_cmd.release() window_commands.quit(force_all_read=False) diff --git a/examples/looping/cam.py b/examples/looping/cam.py new file mode 100644 index 0000000..a0092ae --- /dev/null +++ b/examples/looping/cam.py @@ -0,0 +1,8 @@ +from displayarray import display +import numpy as np + +arr = np.random.normal(0.5, 0.1, (100, 100, 5)) + +with display(0, size=(-1,-1)) as displayer: + while displayer: + pass diff --git a/examples/looping/random_display.py b/examples/looping/random_display.py index 5d7b67b..2fdd78c 100644 --- a/examples/looping/random_display.py +++ b/examples/looping/random_display.py @@ -1,9 +1,14 @@ from displayarray import display import numpy as np -arr = np.random.normal(0.5, 0.1, (100, 100, 5)) +arr = np.random.normal(0.5, 0.1, (100, 100, 3)) +arr2 = np.random.normal(0.5, 0.1, (200, 200, 3)) +arr3 = np.random.normal(0.5, 0.1, (300, 300, 3)) with display(arr) as displayer: while displayer: - arr[:] += np.random.normal(0.001, 0.0005, (100, 100, 5)) + arr[:] += np.random.normal(0.001, 0.0005, (100, 100, 3)) arr %= 1.0 + displayer.update(arr2, '2') + displayer.update(arr3, '3') + diff --git a/setup.py b/setup.py index 7b71cf7..ae3d820 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ if os.path.exists(readme_path): setup( long_description=readme, name="displayarray", - version="1.3.1", + version="2.0.0", description="Tool for displaying numpy arrays.", python_requires="==3.*,>=3.7.0", project_urls={"repository": "https://github.com/simleek/displayarray"},