diff --git a/docs/user_guide/video.md b/docs/user_guide/video.md index 85e1b84..e474669 100644 --- a/docs/user_guide/video.md +++ b/docs/user_guide/video.md @@ -2,8 +2,6 @@ Human friendly interface to the Video for Linux 2 (V4L2) subsystem. -![V4L2 demo](video.svg) - Without further ado:
@@ -89,6 +87,57 @@ finally: camera.close() ``` +## Finding devices + +Get a random video device: + +```python +>>> from linuxpy.video.device import find + +>>> dev = find() + +``` + +All video devices: + +```python +>>> +>>> devs = list(find(find_all=True)) +[, + , + , + , + ] +``` + +List of all capture devices: + +```python +>>> from linuxpy.video.device import iter_video_capture_devices +>>> list(iter_video_capture_devices()) +[, + , + ] +``` + +Custom filter: find a uvc device: + +```python +>>> find(custom_match=lambda d: "uvc" in d.info.driver) + +``` + +Custom filter: find all uvc devices: + +```python +>>> list(find(find_all=True, custom_match=lambda d: "uvc" in d.info.driver)) +[, + , + , + ] +``` + + ## Capture Simple capture without any configuration is possible using the Device object @@ -113,27 +162,81 @@ from linuxpy.video.device import Device, VideoCapture with Device.from_id(0) as camera: capture = VideoCapture(camera) capture.set_format(640, 480, "MJPG") + capture.set_fps(10) with capture: for frame in capture: ... ``` -Note that `VideoCapture` configuration must be done **before** the capture is started -(ie, the `with capture:` statement.) +which is roughly equivalent to: + +```python +with Device.from_id(0) as camera: + capture = VideoCapture(camera) + capture.set_format(640, 480, "MJPG") + capture.set_fps(10) + capture.arm() + try: + capture.start() + try: + for frame in capture: + print(frame) + finally: + capture.stop() + finally: + capture.disarm() +``` + +Note that `VideoCapture` configuration must be done **before** the camera is armed +(ie, before them arm() call or entering the `with capture:` statement.) By default, VideoCapture will use memory map if the device has STREAMING capability and falls back to standard read if not. It is also possible to force a specific reader: ```python -from linuxpy.video.device import Capability, Device, VideoCapture +from linuxpy.video.device import Capability, Device, VideoCapture, ReadSource with Device.from_id(0) as cam: - with VideoCapture(cam, source=Capability.READWRITE): + with VideoCapture(cam, buffer_type=ReadSource): for frame in capture: ... ``` +The previous example will grab frame data using the linux read system call. + +### Frame + +The frame object contains metadata concerning the current (width, height, pixel format, +size, timestamp, frame_nb) as well as the frame data. +Unless the VideoCapture uses a custom buffer type for the acquisition, the `frame.data` +represents the immutable data for the specific frame and it is safe to use without any +additional copies. + +```python +>>> # Acquire one frame +>>> with Device.from_id(0) as cam: + stream = iter(cam) + frame = next(frame) + +>>> frame + +>>> frame.width, frame.height, frame.pixel_format +(640, 480, ) + +>>> frame.format +Format(width=640, height=480, pixel_format=, size=614989, bytes_per_line=0) + +>>> len(frame.data) +61424 + +>>> frame.data is bytes(frame) +True + +>>> frame.array +array([255, 216, 255, ..., 0, 0, 0], shape=(61424,), dtype=uint8) +``` + ## Information Getting information about the device: @@ -146,7 +249,7 @@ Getting information about the device: >>> cam.info.card 'Integrated_Webcam_HD: Integrate' ->>> cam.info.capabilities +>>> cam.info.device_capabilities >>> cam.info.formats @@ -157,32 +260,50 @@ Getting information about the device: >>> cam.get_format(BufferType.VIDEO_CAPTURE) Format(width=640, height=480, pixelformat=} +``` + +## Controls + +Device controls can be accessed on the `controls` member of the `Device` object. +This object works like a dict. The keys can be control name or control id. The value is +a control object containing the appropiate fields dependent on the type of control + +Show list of available controls: +```python >>> for ctrl in cam.controls.values(): print(ctrl) - - - - - - - - +... +``` + +Access a control with python dict syntax by control name: +```python >>> cam.controls["saturation"] +``` ->>> cam.controls["saturation"].id +Retrieve control information: + +```python +>>> saturation = cam.controls["saturation"] +>>> saturation.id 9963778 + >>> cam.controls[9963778] ->>> cam.controls.brightness - +>>> saturation.flags + +``` + +Access controls using object attribute syntax: + +```python >>> cam.controls.brightness.value = 64 >>> cam.controls.brightness diff --git a/examples/video/qt/grid.py b/examples/video/qt/grid.py index 5ae2ad8..6f63e17 100644 --- a/examples/video/qt/grid.py +++ b/examples/video/qt/grid.py @@ -3,7 +3,7 @@ from qtpy import QtWidgets -from linuxpy.video.device import Device +from linuxpy.video.device import iter_video_capture_devices from linuxpy.video.qt import QCamera, QVideoWidget try: @@ -41,27 +41,30 @@ def close(): layout = QtWidgets.QGridLayout(grid) grid.setLayout(layout) - COLUMNS, ROWS = 6, 3 - qcameras = set() - for row in range(ROWS): - for column in range(COLUMNS): - dev_id = column + row * COLUMNS - if dev_id > 15: - break - dev_id += 10 - device = Device.from_id(dev_id) - device.open() - qcamera = QCamera(device) - widget = QVideoWidget(qcamera) - layout.addWidget(widget, row, column) - qcameras.add(qcamera) + devices = sorted(iter_video_capture_devices(), key=lambda d: d.index) + n = len(devices) + if n < 5: + cols = 2 + elif n < 7: + cols = 3 + elif n < 17: + cols = 4 + else: + cols = 5 - d0 = Device.from_id(0) - d0.open() - qcamera = QCamera(d0) - qcameras.add(qcamera) - widget = QVideoWidget(qcamera) - layout.addWidget(widget) + row, col = 0, 0 + qcameras = set() + for device in devices: + device.open() + qcamera = QCamera(device) + widget = QVideoWidget(qcamera) + layout.addWidget(widget, row, col) + qcameras.add(qcamera) + if col < cols - 1: + col += 1 + else: + col = 0 + row += 1 window.show() app.exec() diff --git a/examples/video/qt/pyqtgraph1.py b/examples/video/qt/pyqtgraph1.py index 3993329..d69cf01 100644 --- a/examples/video/qt/pyqtgraph1.py +++ b/examples/video/qt/pyqtgraph1.py @@ -1,12 +1,11 @@ import contextlib -import functools import logging -from pyqtgraph import GraphicsLayout, GraphicsView, ImageItem, setConfigOption +from pyqtgraph import GraphicsLayout, GraphicsView, setConfigOption from qtpy import QtWidgets -from linuxpy.video.device import BufferType, Device, PixelFormat -from linuxpy.video.qt import QCamera, QVideoControls +from linuxpy.video.device import BufferType, PixelFormat, iter_video_capture_devices +from linuxpy.video.qt import QCamera, QVideoControls, QVideoItem, QVideoStream def main(): @@ -32,7 +31,8 @@ def update_image(image_item, frame): logging.basicConfig(level=args.log_level.upper(), format=fmt) app = QtWidgets.QApplication([]) with contextlib.ExitStack() as stack: - devices = [stack.enter_context(Device.from_id(i)) for i in range(10, 24)] + idevices = iter_video_capture_devices() + devices = sorted((stack.enter_context(dev) for dev in idevices), key=lambda dev: dev.index) for device in devices: device.set_format(BufferType.VIDEO_CAPTURE, 640, 480, PixelFormat.RGB24) device.set_fps(BufferType.VIDEO_CAPTURE, 15) @@ -53,21 +53,24 @@ def update_image(image_item, frame): video_layout.addWidget(view) n = len(devices) - if n < 3: - cols = 1 - elif n < 9: + if n < 5: cols = 2 - elif n < 16: + elif n < 7: cols = 3 - else: + elif n < 17: cols = 4 + else: + cols = 5 for i, camera in enumerate(cameras, start=1): controls = QVideoControls(camera) controls_layout.addWidget(controls) view_box = view_layout.addViewBox(name=f"Camera {camera.device.index}", lockAspect=True, invertY=True) - image_item = ImageItem() + # image_item = ImageItem() + # camera.frameChanged.connect(functools.partial(update_image, image_item)) + image_item = QVideoItem() + image_item.stream = QVideoStream(camera) + image_item.stream.imageChanged.connect(image_item.on_image_changed) view_box.addItem(image_item) - camera.frameChanged.connect(functools.partial(update_image, image_item)) if not i % cols: view_layout.nextRow() diff --git a/linuxpy/video/device.py b/linuxpy/video/device.py index 79a39f0..da09b70 100644 --- a/linuxpy/video/device.py +++ b/linuxpy/video/device.py @@ -106,9 +106,9 @@ def human_pixel_format(ifmt): FrameType = collections.namedtuple("FrameType", "type pixel_format width height min_fps max_fps step_fps") -Input = collections.namedtuple("InputType", "index name type audioset tuner std status capabilities") +Input = collections.namedtuple("Input", "index name type audioset tuner std status capabilities") -Output = collections.namedtuple("OutputType", "index name type audioset modulator std capabilities") +Output = collections.namedtuple("Output", "index name type audioset modulator std capabilities") Standard = collections.namedtuple("Standard", "index id name frameperiod framelines") @@ -438,9 +438,10 @@ def free_buffers(fd, buffer_type: BufferType, memory: Memory) -> raw.v4l2_reques return req -def export_buffer(fd, buffer_type: BufferType, index: int) -> int: +def export_buffer(fd, buffer_type: BufferType, index: int) -> raw.v4l2_exportbuffer: req = raw.v4l2_exportbuffer(type=buffer_type, index=index) - return ioctl(fd, IOC.EXPBUF, req).fd + ioctl(fd, IOC.EXPBUF, req) + return req def create_buffers(fd, format: raw.v4l2_format, memory: Memory, count: int) -> raw.v4l2_create_buffers: @@ -459,10 +460,13 @@ def set_raw_format(fd, fmt: raw.v4l2_format): return ioctl(fd, IOC.S_FMT, fmt) -def set_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str = "MJPG"): +def set_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str | int | PixelFormat = "MJPG"): fmt = raw.v4l2_format() if isinstance(pixel_format, str): - pixel_format = raw.v4l2_fourcc(*pixel_format) + try: + pixel_format = PixelFormat[pixel_format] + except KeyError: + pixel_format = raw.v4l2_fourcc(*pixel_format) fmt.type = buffer_type fmt.fmt.pix.pixelformat = pixel_format fmt.fmt.pix.field = Field.ANY @@ -501,12 +505,16 @@ def get_format(fd, buffer_type) -> Union[Format, MetaFmt]: def try_raw_format(fd, fmt: raw.v4l2_format): ioctl(fd, IOC.TRY_FMT, fmt) + return fmt -def try_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str = "MJPG"): +def try_format(fd, buffer_type: BufferType, width: int, height: int, pixel_format: str | int | PixelFormat = "MJPG"): fmt = raw.v4l2_format() if isinstance(pixel_format, str): - pixel_format = raw.v4l2_fourcc(*pixel_format) + try: + pixel_format = PixelFormat[pixel_format] + except KeyError: + pixel_format = raw.v4l2_fourcc(*pixel_format) fmt.type = buffer_type fmt.fmt.pix.pixelformat = pixel_format fmt.fmt.pix.field = Field.ANY @@ -1487,24 +1495,32 @@ def __repr__(self): @property def raw_capabilities(self) -> raw.v4l2_capability: + """ + Capabilities as read from the device. + It caches the capabilities so it only reads from the device once + """ if self._raw_capabilities_cache is None: self._raw_capabilities_cache = read_capabilities(self.device) return self._raw_capabilities_cache @property def driver(self) -> str: + """The device driver name""" return self.raw_capabilities.driver.decode() @property def card(self) -> str: + """The device card name""" return self.raw_capabilities.card.decode() @property def bus_info(self) -> str: + """The device bus info""" return self.raw_capabilities.bus_info.decode() @property def version_tuple(self) -> tuple: + """The driver version tuple in format (a, b, c)""" caps = self.raw_capabilities return ( (caps.version & 0xFF0000) >> 16, @@ -1514,18 +1530,22 @@ def version_tuple(self) -> tuple: @property def version(self) -> str: + """The driver version string in format a.b.c""" return ".".join(map(str, self.version_tuple)) @property def capabilities(self) -> Capability: + """The capabilities""" return Capability(self.raw_capabilities.capabilities) @property def device_capabilities(self) -> Capability: + """The device capabilities""" return Capability(self.raw_capabilities.device_caps) @property def buffers(self): + """Available buffer types according to the device capabilities""" dev_caps = self.device_capabilities return [typ for typ in BufferType if Capability[typ.name] in dev_caps] @@ -1534,6 +1554,7 @@ def get_crop_capabilities(self, buffer_type: BufferType) -> CropCapability: @property def crop_capabilities(self) -> dict[BufferType, CropCapability]: + """Crop capabilities grouped by buffer type for the current active input""" buffer_types = CROP_BUFFER_TYPES & set(self.buffers) result = {} for buffer_type in buffer_types: @@ -1545,6 +1566,7 @@ def crop_capabilities(self) -> dict[BufferType, CropCapability]: @property def formats(self): + """List of supported formats for the current active input""" img_fmt_buffer_types = IMAGE_FORMAT_BUFFER_TYPES & set(self.buffers) return [ image_format @@ -1553,35 +1575,43 @@ def formats(self): ] def buffer_formats(self, buffer_type) -> list[ImageFormat]: + """List of supported formats for the given buffer type for the current active input""" return list(iter_read_formats(self.device, buffer_type)) def format_frame_sizes(self, pixel_format) -> list[FrameSize]: + """List of frame sizes for the given pixel format for the current active input""" return list(iter_read_frame_sizes(self.device, pixel_format)) def frame_sizes(self): + """List of frame sizes for the current active input""" results = [] for fmt in self.formats: results.extend(self.format_frame_sizes(fmt.pixel_format)) return results def fps_intervals(self, pixel_format, width, height) -> list[FrameType]: + """List of available FPS for the given pixel format, width and height for the current active input""" return list(iter_read_frame_intervals(self.device, pixel_format, width, height)) @property - def frame_types(self): + def frame_types(self) -> list[FrameType]: + """List of all available discrete frame types for the current active input""" pixel_formats = {fmt.pixel_format for fmt in self.formats} return list(iter_read_pixel_formats_frame_intervals(self.device, pixel_formats)) @property def inputs(self) -> list[Input]: + """List of available inputs""" return list(iter_read_inputs(self.device)) @property - def outputs(self): + def outputs(self) -> list[Output]: + """List of available outputs""" return list(iter_read_outputs(self.device)) @property def controls(self): + """List of available controls""" return list(iter_read_controls(self.device)) @property