Skip to content
This repository was archived by the owner on Apr 26, 2023. It is now read-only.

Camera capture #18

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ good-names = [
# PyQt methods
"closeEvent", "paintEvent", "keyPressEvent", "mousePressEvent", "mouseMoveEvent", "mouseReleaseEvent",
# https://github.com/PyCQA/pylint/issues/2018
"x", "y", "a0", "i", "t0", "t1"]
"id", "x", "y", "a0", "i", "t0", "t1"]
disable = [
"missing-docstring",
# We group imports
Expand Down
11 changes: 6 additions & 5 deletions src/AutoSplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from PyQt6 import QtCore, QtGui, QtTest
from PyQt6.QtWidgets import QApplication, QFileDialog, QMainWindow, QMessageBox, QWidget
from win32 import win32gui
from capture_method import CaptureMethod
from capture_method import DisplayCaptureMethod

import error_messages
import user_profile
Expand Down Expand Up @@ -125,6 +125,7 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow):
reset_image: Optional[AutoSplitImage] = None
split_images: list[AutoSplitImage] = []
split_image: AutoSplitImage
camera: Optional[cv2.VideoCapture] = None

def __init__(self, parent: Optional[QWidget] = None): # pylint: disable=too-many-statements
super().__init__(parent)
Expand Down Expand Up @@ -248,9 +249,9 @@ def __live_image_function(self):
self.live_image.clear()
return
# Set live image in UI
if self.hwnd or self.windows_graphics_capture:
capture = capture_region(self)
set_ui_image(self.live_image, capture, False)
# if self.hwnd or self.windows_graphics_capture:
capture = capture_region(self)
set_ui_image(self.live_image, capture, False)

def __load_start_image(self, started_by_button: bool = False, wait_for_delay: bool = True):
"""
Expand Down Expand Up @@ -753,7 +754,7 @@ def __get_capture_for_comparison(self):

# This most likely means we lost capture (ie the captured window was closed, crashed, etc.)
# We can't recover by name (yet) with WindowsGraphicsCapture
if capture is None and self.settings_dict["capture_method"] != CaptureMethod.WINDOWS_GRAPHICS_CAPTURE:
if capture is None and self.settings_dict["capture_method"] != DisplayCaptureMethod.WINDOWS_GRAPHICS_CAPTURE:
# Try to recover by using the window name
self.live_image.setText("Trying to recover window...")
hwnd = win32gui.FindWindow(None, self.settings_dict["captured_window_title"])
Expand Down
84 changes: 62 additions & 22 deletions src/capture_method.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
from typing import TypedDict

from dataclasses import dataclass
from platform import version
from collections import OrderedDict
from enum import Enum, unique
from enum import Enum, EnumMeta, unique

import cv2


# https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturepicker#applies-to
WCG_MIN_BUILD = 17134
WCG_MIN_BUILD = 999999 # TODO: Change to 17134 once implemented


class CaptureMethodInfo(TypedDict):
@dataclass
class DisplayCaptureMethodInfo():
name: str
short_description: str
description: str


class DisplayCaptureMethodMeta(EnumMeta):
# Allow checking if simple string is enum
def __contains__(cls, other: str): # noqa:N805
try:
# pyright: reportGeneralTypeIssues=false
cls(other) # pylint: disable=no-value-for-parameter
except ValueError:
return False
else:
return True


@unique
class CaptureMethod(Enum):
class DisplayCaptureMethod(Enum, metaclass=DisplayCaptureMethodMeta):
# Allow TOML to save as a simple string
def __repr__(self):
return self.value
Expand All @@ -34,8 +50,13 @@ def __hash__(self):
DESKTOP_DUPLICATION = "DESKTOP_DUPLICATION"


CAPTURE_METHODS = OrderedDict({
CaptureMethod.BITBLT: CaptureMethodInfo(
class DisplayCaptureMethodDict(OrderedDict[DisplayCaptureMethod, DisplayCaptureMethodInfo]):
def get_method_by_index(self, index: int):
return list(self.keys())[index]


DISPLAY_CAPTURE_METHODS = DisplayCaptureMethodDict({
DisplayCaptureMethod.BITBLT: DisplayCaptureMethodInfo(
name="BitBlt",
short_description="fast, issues with Hardware Acceleration and OpenGL",
description=(
Expand All @@ -44,7 +65,7 @@ def __hash__(self):
"\nbut it cannot properly record OpenGL or Hardware Accelerated Windows. "
),
),
CaptureMethod.WINDOWS_GRAPHICS_CAPTURE: CaptureMethodInfo(
DisplayCaptureMethod.WINDOWS_GRAPHICS_CAPTURE: DisplayCaptureMethodInfo(
name="Windows Graphics Capture",
short_description="fastest, most compatible but less features",
description=(
Expand All @@ -57,7 +78,7 @@ def __hash__(self):
"\nfor more details about those restrictions."
),
),
CaptureMethod.DESKTOP_DUPLICATION: CaptureMethodInfo(
DisplayCaptureMethod.DESKTOP_DUPLICATION: DisplayCaptureMethodInfo(
name="Direct3D Desktop Duplication",
short_description="very slow, bound to display",
description=(
Expand All @@ -67,7 +88,7 @@ def __hash__(self):
"\noverlapping windows will show up and can't record across displays. "
),
),
CaptureMethod.PRINTWINDOW_RENDERFULLCONTENT: CaptureMethodInfo(
DisplayCaptureMethod.PRINTWINDOW_RENDERFULLCONTENT: DisplayCaptureMethodInfo(
name="Force Full Content Rendering",
short_description="very slow, can affect rendering pipeline",
description=(
Expand All @@ -80,20 +101,39 @@ def __hash__(self):
})


def get_capture_method_index(capture_method: CaptureMethod):
"""
Returns 0 if the capture_method is invalid or unsupported
"""
try:
return list(CAPTURE_METHODS.keys()).index(capture_method)
except ValueError:
return 0
# Detect and remove unsupported capture methods
if int(version().split(".")[2]) < WCG_MIN_BUILD:
DISPLAY_CAPTURE_METHODS.pop(DisplayCaptureMethod.WINDOWS_GRAPHICS_CAPTURE)


def get_capture_method_by_index(index: int):
return list(CAPTURE_METHODS.keys())[index]
@dataclass
class CameraInfo():
id: int
name: str
occupied: str


def get_all_cameras():
index = 0
video_captures: list[CameraInfo] = []
while index < 8:
video_capture = cv2.VideoCapture(index) # pyright: ignore
video_capture.setExceptionMode(True)
try:
# https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html#ga023786be1ee68a9105bf2e48c700294d
print(video_capture.getBackendName()) # pyright: ignore
video_capture.grab()
except cv2.error as error: # pyright: ignore
if error.code == cv2.Error.STS_ERROR:
video_captures.append(CameraInfo(index, f"Camera {index}", False))
else:
video_captures.append(CameraInfo(index, f"Camera {index}", True))

video_capture.release()
index += 1
return video_captures


# Detect and remove unsupported capture methods
if int(version().split(".")[2]) < WCG_MIN_BUILD:
CAPTURE_METHODS.pop(CaptureMethod.WINDOWS_GRAPHICS_CAPTURE)
DISPLAY_CAPTURE_METHODS.pop(DisplayCaptureMethod.WINDOWS_GRAPHICS_CAPTURE)
27 changes: 20 additions & 7 deletions src/capture_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
from PyQt6 import QtCore, QtGui
from PyQt6.QtWidgets import QLabel
from win32 import win32gui
from win32typing import PyCBitmap, PyCDC
from winsdk.windows.graphics.imaging import SoftwareBitmap, BitmapBufferAccessMode

from capture_method import CaptureMethod
from capture_method import DisplayCaptureMethod
from screen_region import WindowsGraphicsCapture

# This is an undocumented nFlag value for PrintWindow
Expand All @@ -41,14 +40,14 @@ def __bit_blt_capture(hwnd: int, selection: Region, render_full_content: bool =
# If the window closes while it's being manipulated, it could cause a crash
try:
window_dc: int = win32gui.GetWindowDC(hwnd)
dc_object: PyCDC = win32ui.CreateDCFromHandle(window_dc)
dc_object = win32ui.CreateDCFromHandle(window_dc)

# Causes a 10-15x performance drop. But allows recording hardware accelerated windows
if render_full_content:
ctypes.windll.user32.PrintWindow(hwnd, dc_object.GetSafeHdc(), PW_RENDERFULLCONTENT)

compatible_dc = dc_object.CreateCompatibleDC()
bitmap: PyCBitmap = win32ui.CreateBitmap()
bitmap = win32ui.CreateBitmap()
bitmap.CreateCompatibleBitmap(dc_object, selection["width"], selection["height"])
compatible_dc.SelectObject(bitmap)
compatible_dc.BitBlt(
Expand Down Expand Up @@ -117,6 +116,17 @@ async def coroutine():
return image


def __camera_capture(camera: Optional[cv2.VideoCapture], selection: Region):
if not camera:
return None
result, image = camera.read()
if not result:
return None
image = image[selection["x"]:selection["width"] + selection["x"],
selection["y"]:selection["width"] + selection["y"]]
return cv2.cvtColor(image, cv2.COLOR_BGR2BGRA)


def capture_region(autosplit: AutoSplit):
"""
Captures an image of the region for a window matching the given
Expand All @@ -130,13 +140,16 @@ def capture_region(autosplit: AutoSplit):
selection = autosplit.settings_dict["capture_region"]
capture_method = autosplit.settings_dict["capture_method"]

if capture_method == CaptureMethod.WINDOWS_GRAPHICS_CAPTURE:
if capture_method not in DisplayCaptureMethod:
return __camera_capture(autosplit.camera, selection)

if capture_method == DisplayCaptureMethod.WINDOWS_GRAPHICS_CAPTURE:
return __windows_graphics_capture(autosplit.windows_graphics_capture, selection)

if capture_method == CaptureMethod.DESKTOP_DUPLICATION:
if capture_method == DisplayCaptureMethod.DESKTOP_DUPLICATION:
return __d3d_capture(hwnd, selection)

return __bit_blt_capture(hwnd, selection, capture_method == CaptureMethod.PRINTWINDOW_RENDERFULLCONTENT)
return __bit_blt_capture(hwnd, selection, capture_method == DisplayCaptureMethod.PRINTWINDOW_RENDERFULLCONTENT)


def set_ui_image(qlabel: QLabel, image: Optional[cv2.ndarray], transparency: bool):
Expand Down
Loading