diff --git a/core/__init__.py b/core/__init__.py index 4e04b58..86a7fc6 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -25,7 +25,8 @@ class StreamNodeHeader(CozyImageNode): return deep_merge(d, { "optional": { - # "WH": ("VEC2INT", {"default": [640, 480], "mij": 160, "tooltip": "width and height"}), + "FLIP": ("BOOLEAN", {"default": False, "tooltip": "Flip image top-to-bottom"}), + "REVERSE": ("BOOLEAN", {"default": False, "tooltip": "reverse image left-to-right"}), "FPS": ("INT", {"default": 30, "min": 1, "max": 60, "tooltip": "Framerate to attempt when capturing"}), "BATCH": ("INT", {"default": 1, "min": 1, diff --git a/core/node_monitor.py b/core/node_monitor.py index 2f61cfc..4a8cc14 100644 --- a/core/node_monitor.py +++ b/core/node_monitor.py @@ -96,12 +96,14 @@ Capture frames from a desktop monitor. Supports batch processing, allowing multi fps = parse_param(kw, "FPS", EnumConvertType.INT, 30) xy = parse_param(kw, "XY", EnumConvertType.VEC2INT, [(0,0)], 0) wh = parse_param(kw, "WH", EnumConvertType.VEC2INT, [(0,0)], 0) + flip = parse_param(kw, "FLIP", EnumConvertType.BOOLEAN, False) + reverse = parse_param(kw, "REVERSE", EnumConvertType.BOOLEAN, False) pbar = ProgressBar(batch_size) size = [batch_size] * batch_size - params = list(zip_longest_fill(monitor, fps, xy, wh, size)) + params = list(zip_longest_fill(monitor, fps, xy, wh, flip, reverse, size)) with mss.mss() as screen: - for idx, (monitor, fps, xy, wh, size) in enumerate(params): + for idx, (monitor, fps, xy, wh, flip, reverse, size) in enumerate(params): try: monitor = int(monitor.split('-')[0].strip()) @@ -127,9 +129,12 @@ Capture frames from a desktop monitor. Supports batch processing, allowing multi 'width': width, 'height': height } - print(region) img = screen.grab(region) img = cv2.cvtColor(np.array(img, dtype=np.uint8), cv2.COLOR_RGB2BGR) + if flip: + img = cv2.flip(img, 0) + if reverse: + img = cv2.flip(img, 1) images.append(cv_to_tensor_full(img)) pbar.update_absolute(idx) diff --git a/core/node_remote.py b/core/node_remote.py index 0f1f326..813abc1 100644 --- a/core/node_remote.py +++ b/core/node_remote.py @@ -1,12 +1,10 @@ """ -Jovi_Capture - http://www.github.com/amorano/Jovi_Capture -REMOTE -- Capture remove URL +Capture remote URL """ import time -from typing import Dict, Tuple +from typing import Dict -import cv2 import torch from comfy.utils import ProgressBar @@ -14,17 +12,19 @@ from comfy.utils import ProgressBar from cozy_comfyui import \ EnumConvertType, \ logger, \ - deep_merge, parse_param + deep_merge, parse_param, zip_longest_fill +from cozy_comfyui import RGBAMaskType from cozy_comfyui.image.convert import cv_to_tensor_full -from . import StreamNodeHeader +from . import VideoStreamNodeHeader +from .stream import MediaStreamBase # ============================================================================== # === NODE === # ============================================================================== -class RemoteSteamReader(StreamNodeHeader): +class RemoteSteamReader(VideoStreamNodeHeader): NAME = "REMOTE" DESCRIPTION = """ Capture frames from a URL. Supports batch processing, allowing multiple frames to be captured simultaneously. The node provides options for configuring the source, resolution, frame rate, zoom, orientation, and interpolation method. Additionally, it supports capturing frames from multiple monitors or windows simultaneously. @@ -40,55 +40,43 @@ Capture frames from a URL. Supports batch processing, allowing multiple frames t } }, d) - def __init__(self, *arg, **kw) -> None: - super().__init__(*arg, **kw) - self.__url = "" - self.__device = None # MediaStreamURL + def run(self, **kw) -> RGBAMaskType: + # need to see if we have a device... + if self.device is None: + self.device = MediaStreamBase() - def run(self, **kw) -> Tuple[torch.Tensor, torch.Tensor]: - wait = parse_param(kw, "WAIT", EnumConvertType.BOOLEAN, False)[0] - if wait: - return self.__last images = [] - batch_size, rate = parse_param(kw, "BATCH", EnumConvertType.VEC2INT, [(1, 30)], 1)[0] + self.device.url = parse_param(kw, "URL", EnumConvertType.STRING, "")[0] + self.device.fps = parse_param(kw, "FPS", EnumConvertType.INT, 30)[0] + batch_size = parse_param(kw, "BATCH", EnumConvertType.INT, 1, 1)[0] + if parse_param(kw, "PAUSE", EnumConvertType.BOOLEAN, False)[0]: + self.device.pause() + else: + self.device.play() + self.device.timeout = parse_param(kw, "TIMEOUT", EnumConvertType.INT, 5, 1, 30)[0] + flip = parse_param(kw, "FLIP", EnumConvertType.BOOLEAN, False) + reverse = parse_param(kw, "REVERSE", EnumConvertType.BOOLEAN, False) + + rate = 1. / self.device.fps pbar = ProgressBar(batch_size) - rate = 1. / rate + batch_size = [batch_size] * batch_size + params = list(zip_longest_fill(batch_size, flip, reverse)) + for idx, (batch_size, flip, reverse) in enumerate(params): + start_time = time.perf_counter() + self.device.flip = flip + self.device.reverse = reverse + while True: + if not (img := self.device.frame) is None and img.sum() > 0: + break + if time.perf_counter() - start_time > self.device.timeout: + logger.error("could not capture device") + img = self.empty + break - url = parse_param(kw, "URL", EnumConvertType.STRING, "")[0] - url = url.split('-')[0].strip() - try: - _ = int(url) - url = str(url) - except: url = "" - - # timeout and try again? - if self.__capturing > 0 and time.perf_counter() - self.__capturing > 3000: - logger.error(f'timed out {self.__url}') - self.__capturing = 0 - self.__url = "" - - if self.__device is not None: - self.__capturing = 0 - - if wait: - self.__device.pause() - else: - self.__device.play() - - fps = parse_param(kw, "FPS", EnumConvertType.INT, 30)[0] - self.__device.fps = fps - self.__device.zoom = parse_param(kw, "ZOOM", EnumConvertType.FLOAT, 0, 0, 1)[0] - - for idx in range(batch_size): - img = self.__device.frame - if img is None: - images.append(self.__empty) - else: - img = cv2.cvtColor(img, cv2.COLOR_RGB2BGRA) - images.append(cv_to_tensor_full(img)) - pbar.update_absolute(idx) - if batch_size > 1: - time.sleep(rate) + images.append(cv_to_tensor_full(img)) + if batch_size > 1: + time.sleep(rate) + pbar.update_absolute(idx) if len(images) == 0: images.append(self.__empty) diff --git a/core/node_webcam.py b/core/node_webcam.py index 17c1670..ed07bc7 100644 --- a/core/node_webcam.py +++ b/core/node_webcam.py @@ -15,7 +15,7 @@ from server import PromptServer from cozy_comfyui import \ EnumConvertType, \ logger, \ - deep_merge, parse_param + deep_merge, parse_param, zip_longest_fill from cozy_comfyui import RGBAMaskType from cozy_comfyui.image.convert import cv_to_tensor_full @@ -38,7 +38,7 @@ def camera_list() -> List[str]: idx = 0 failed = 0 camera_list = [] - while failed < 2: + while failed < 1: cap = cv2.VideoCapture(idx) if cap.isOpened(): w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) @@ -60,9 +60,11 @@ def camera_list() -> List[str]: @PromptServer.instance.routes.get(f"/{PACKAGE.lower()}/camera") async def route_cameraList(req) -> Any: - # load the camera list here.. + force = req.query_string == "force=true" + if force and not JOV_SCAN_DEVICES: + return web.json_response(["NONE"]) CameraStreamReader.CAMERAS = camera_list() - return web.json_response(CameraStreamReader.CAMERAS) + return web.json_response(CameraStreamReader.CAMERAS, content_type='application/json') # ============================================================================== # === CLASS === @@ -74,28 +76,8 @@ class MediaStreamCamera(MediaStreamBase): self.__focus = 0 self.__exposure = 1 self.__zoom = 0 - self.__flip: bool = False super().__init__(fps=fps) - @property - def frame(self): - frame = super().frame - try: - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGRA) - if self.__flip: - frame = cv2.flip(frame, 1) - except: - pass - return frame - - @property - def flip(self) -> bool: - return self.__flip - - @flip.setter - def flip(self, flip: bool) -> None: - self.__flip = flip - @property def zoom(self) -> float: return self.__zoom @@ -146,12 +128,11 @@ Capture frames from a web camera. Supports batch processing, allowing multiple f d = super().INPUT_TYPES() if cls.CAMERAS is None: - cls.CAMERAS = camera_list() if JOV_SCAN_DEVICES else ["NONE"] + cls.CAMERAS = camera_list() if JOV_SCAN_DEVICES else ["0 -NONE"] return deep_merge({ "optional": { "CAMERA": (cls.CAMERAS, {"default": cls.CAMERAS[0], "tooltip": "The camera from the auto-scanned list"}), - "FLIP": ("BOOLEAN", {"default": False, "tooltip": "Camera flip image left-to-right"}), "ZOOM": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1, "tooltip": "Camera zoom"}), "FOCUS": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1, "tooltip": "Camera focus"}), "EXPOSURE": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1, "tooltip": "Camera exposure"}) @@ -181,16 +162,20 @@ Capture frames from a web camera. Supports batch processing, allowing multiple f self.device.pause() else: self.device.play() - - self.device.flip = parse_param(kw, "FLIP", EnumConvertType.BOOLEAN, False)[0] self.device.zoom = parse_param(kw, "ZOOM", EnumConvertType.INT, 0, 0, 100)[0] / 100. self.device.focus = parse_param(kw, "FOCUS", EnumConvertType.INT, 0, 0, 100)[0] / 100. self.device.exposure = parse_param(kw, "EXPOSURE", EnumConvertType.INT, 0, 0, 100)[0] / 100. + flip = parse_param(kw, "FLIP", EnumConvertType.BOOLEAN, False) + reverse = parse_param(kw, "REVERSE", EnumConvertType.BOOLEAN, False) rate = 1. / self.device.fps pbar = ProgressBar(batch_size) - for idx in range(batch_size): + batch_size = [batch_size] * batch_size + params = list(zip_longest_fill(batch_size, flip, reverse)) + for idx, (batch_size, flip, reverse) in enumerate(params): start_time = time.perf_counter() + self.device.flip = flip + self.device.reverse = reverse while True: if not (img := self.device.frame) is None and img.sum() > 0: break diff --git a/core/node_window.py b/core/node_window.py index b27b449..797d0af 100644 --- a/core/node_window.py +++ b/core/node_window.py @@ -6,6 +6,7 @@ import time import platform from typing import Any, Dict, Optional, Tuple +import cv2 import torch import numpy as np import pywinctl as pwc @@ -109,7 +110,7 @@ def window_capture(hwnd: int, client_area_only: bool=False, region: Optional[Tup width = max_width if rw == 0 else min(rw, max_width - x) height = max_height if rh == 0 else min(rh, max_height - y) - logger.info(f"Capture region: pos=({x},{y}) size=({width},{height})") + # logger.info(f"Capture region: pos=({x},{y}) size=({width},{height})") img = None try: @@ -120,17 +121,6 @@ def window_capture(hwnd: int, client_area_only: bool=False, region: Optional[Tup bitmap.CreateCompatibleBitmap(dc, width, height) compatible_dc.SelectObject(bitmap) - # Set the correct source coordinates for BitBlt - """ - compatible_dc.BitBlt( - (0, 0), - (width, height), - dc, - (capture_left, capture_top), - win32con.SRCCOPY - ) - """ - result = windll.user32.PrintWindow(hwnd, compatible_dc.GetSafeHdc(), 0) if result is None: return None @@ -138,6 +128,8 @@ def window_capture(hwnd: int, client_area_only: bool=False, region: Optional[Tup img = np.frombuffer(bmpstr, dtype='uint8') img = img.reshape((height, width, 4)) img = img[y:y+height, x:x+width] + img = img[..., [2, 1, 0, 3]] + except Exception as e: logger.error(e) finally: @@ -256,7 +248,7 @@ def window_capture(hwnd: int, client_area_only: bool=False, region: Optional[Tup @PromptServer.instance.routes.get(f"/{PACKAGE.lower()}/window") async def route_windowList(req) -> Any: WindowStreamReader.WINDOWS = window_list() - return web.json_response(WindowStreamReader.WINDOWS) + return web.json_response(WindowStreamReader.WINDOWS, content_type='application/json') # ============================================================================== # === NODE === @@ -299,10 +291,12 @@ Capture frames from a dekstop window. Supports batch processing, allowing multip xy = parse_param(kw, "XY", EnumConvertType.VEC2INT, [(0,0)], 0) wh = parse_param(kw, "WH", EnumConvertType.VEC2INT, [(0,0)], 0) client = parse_param(kw, "CLIENT", EnumConvertType.BOOLEAN, False) + flip = parse_param(kw, "FLIP", EnumConvertType.BOOLEAN, False) + reverse = parse_param(kw, "REVERSE", EnumConvertType.BOOLEAN, False) pbar = ProgressBar(batch_size) size = [batch_size] * batch_size - params = list(zip_longest_fill(window, fps, xy, wh, client, size)) - for idx, (window, fps, xy, wh, client, size) in enumerate(params): + params = list(zip_longest_fill(window, fps, xy, wh, client, flip, reverse, size)) + for idx, (window, fps, xy, wh, client, flip, reverse, size) in enumerate(params): try: window = self.WINDOWS[window] except Exception as e: @@ -312,6 +306,11 @@ Capture frames from a dekstop window. Supports batch processing, allowing multip region = (xy[0], xy[1], wh[0], wh[1]) if (img := window_capture(window, client, region)) is None: img = self.empty + else: + if flip: + img = cv2.flip(img, 0) + if reverse: + img = cv2.flip(img, 1) images.append(cv_to_tensor_full(img)) if batch_size > 1: diff --git a/core/stream.py b/core/stream.py index a3c6edc..06a0783 100644 --- a/core/stream.py +++ b/core/stream.py @@ -25,14 +25,16 @@ class MediaStreamBase: self.__frame = None self.__source = None self.__url = None + self.__reverse: bool = False + self.__flip: bool = False self.__height = 0 self.__width = 0 self.__thread = threading.Thread(target=self.__run, daemon=True) self.__thread.start() def __run(self) -> None: + start_time = time.perf_counter() while not self.__quit: - start_time = time.perf_counter() if not self.__source or not self.__source.isOpened(): logger.error("waiting on device") time.sleep(0.5) @@ -46,12 +48,17 @@ class MediaStreamBase: while True: ret, frame = self.__source.read() if ret and frame is not None and frame.sum() > 0: + if self.__flip: + frame = cv2.flip(frame, 0) + if self.__reverse: + frame = cv2.flip(frame, 1) + self.__frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGRA) break - if time.perf_counter() - start_time > self.__timeout: + elif time.perf_counter() - start_time > self.__timeout: logger.error("could not capture frame") self.__quit = True break - self.__frame = frame + """ if not ret: count = int(self.__source.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -59,19 +66,24 @@ class MediaStreamBase: if pos >= count: self.__source.set(cv2.CAP_PROP_POS_FRAMES, 0) ret, self.__frame = self.__source.read() + https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4 """ elapsed = time.perf_counter() - start_time time.sleep(max(delta - elapsed, 0)) + start_time = time.perf_counter() self.__end() def __repr__(self) -> str: return self.__class__.__name__ def __end(self) -> None: - self.__quit = True if self.__thread: - self.__thread.join(timeout=self.__timeout) + try: + self.__thread.join(timeout=self.__timeout) + except RuntimeError as e: + pass + self.__quit = True if self.__source is not None: self.__source.release() self.__source = None @@ -99,10 +111,9 @@ class MediaStreamBase: if new_source.isOpened(): self.__source = new_source self.__url = url - logger.info(f"Captured camera device: {self.__url}") - return - - logger.error(f"Failed to open camera source: {url}") + logger.info(f"captured url: {self.__url}") + else: + logger.error(f"failed to open source: {url}") @property def source(self) -> cv2.VideoCapture: @@ -151,3 +162,19 @@ class MediaStreamBase: if self.__source is not None: self.__height = max(1, height) self.__source.set(cv2.CAP_PROP_FRAME_HEIGHT, self.__height) + + @property + def flip(self) -> bool: + return self.__flip + + @flip.setter + def flip(self, flip: bool) -> None: + self.__flip = flip + + @property + def reverse(self) -> bool: + return self.__reverse + + @flip.setter + def reverse(self, reverse: bool) -> None: + self.__reverse = reverse \ No newline at end of file diff --git a/web/node_webcam.js b/web/node_webcam.js index 529a094..8be0c31 100644 --- a/web/node_webcam.js +++ b/web/node_webcam.js @@ -15,18 +15,26 @@ app.registerExtension({ return } - const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { - const me = onNodeCreated?.apply(this); + const refresh_cameras = async(widget, force=false) => { + let url = "/jov_capture/camera" + if (force) { + url += "?force=true"; + } + var data = await api_get(url); + widget.options.values = data; + widget.value = data[0]; + app.canvas.setDirty(true); + } + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = async function () { + const me = onNodeCreated?.apply(this); const widget_camera = this.widgets.find(w => w.name == 'CAMERA'); this.addWidget('button', 'REFRESH CAMERA LIST', 'refresh', async () => { - var data = await api_get("/jov_capture/camera"); - widget_camera.options.values = data; - widget_camera.value = data[0]; - app.canvas.setDirty(true); + await refresh_cameras(widget_camera); }); + await refresh_cameras(widget_camera, true); return me; } diff --git a/web/node_window.js b/web/node_window.js index a4bcb83..2fabf6c 100644 --- a/web/node_window.js +++ b/web/node_window.js @@ -15,19 +15,22 @@ app.registerExtension({ return } - const onNodeCreated = nodeType.prototype.onNodeCreated - nodeType.prototype.onNodeCreated = function () { - const me = onNodeCreated?.apply(this); + const refresh_windows = async(widget) => { + var data = await api_get("/jov_capture/window"); + widget.options.values = Object.keys(data); + widget.value = widget.options.values[0]; + app.canvas.setDirty(true); + } + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = async function () { + const me = onNodeCreated?.apply(this); const widget_window = this.widgets.find(w => w.name == 'WINDOW'); this.addWidget('button', 'REFRESH WINDOW LIST', 'refresh', async () => { - var data = await api_get("/jov_capture/window"); - widget_window.options.values = Object.keys(data); - widget_window.value = widget_window.options.values[0]; - console.info(widget_window) - app.canvas.setDirty(true); + refresh_windows(widget_window); }); + await refresh_windows(widget_window); return me; } diff --git a/web/util_jov.js b/web/util_jov.js index eaa85cc..99a9202 100644 --- a/web/util_jov.js +++ b/web/util_jov.js @@ -37,7 +37,8 @@ export async function api_post(route, id, cmd) { export async function api_get(route) { var response = await api.fetchApi(route, { cache: "no-store" }); - return await response.json() + var text = await response.text(); + return JSON.parse(text); } function widgetHide(node, widget, suffix = '') {