added flip and reverse

fix camera scan for first time with no auto-scan
This commit is contained in:
Alexander G. Morano
2025-02-25 04:24:50 -05:00
parent 7b6df97cd0
commit 335ae69a71
9 changed files with 142 additions and 125 deletions
+2 -1
View File
@@ -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,
+8 -3
View File
@@ -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)
+40 -52
View File
@@ -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)
+14 -29
View File
@@ -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
+14 -15
View File
@@ -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:
+36 -9
View File
@@ -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
+15 -7
View File
@@ -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;
}
+11 -8
View File
@@ -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;
}
+2 -1
View File
@@ -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 = '') {