Skip to content
Merged
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
8 changes: 4 additions & 4 deletions examples/video/qt/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
import cv2
from qtpy import QtCore, QtGui, QtWidgets

from linuxpy.video.device import Capability, Device, PixelFormat, VideoCapture
from linuxpy.video.device import Device, MemoryMap, PixelFormat, ReadSource, VideoCapture

MODES = {
"auto": None,
"mmap": Capability.STREAMING,
"read": Capability.READWRITE,
"mmap": MemoryMap,
"read": ReadSource,
}


Expand Down Expand Up @@ -153,7 +153,7 @@ def update():
timer.timeout.connect(update)

with device:
capture = VideoCapture(device, source=MODES[args.mode], size=args.nb_buffers)
capture = VideoCapture(device, buffer_type=MODES[args.mode], size=args.nb_buffers)
capture.set_fps(args.frame_rate)
capture.set_format(width, height, args.frame_format)
with capture:
Expand Down
3 changes: 2 additions & 1 deletion linuxpy/video/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def human_pixel_format(ifmt):

MetaFmt = collections.namedtuple("MetaFmt", "format max_buffer_size width height bytes_per_line")

Format = collections.namedtuple("Format", "width height pixel_format size")
Format = collections.namedtuple("Format", "width height pixel_format size bytes_per_line")

CropCapability = collections.namedtuple("CropCapability", "type bounds defrect pixel_aspect")

Expand Down Expand Up @@ -495,6 +495,7 @@ def get_format(fd, buffer_type) -> Union[Format, MetaFmt]:
height=f.fmt.pix.height,
pixel_format=PixelFormat(f.fmt.pix.pixelformat),
size=f.fmt.pix.sizeimage,
bytes_per_line=f.fmt.pix.bytesperline,
)


Expand Down
94 changes: 64 additions & 30 deletions linuxpy/video/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,18 @@ def register(self, camera, type):
event_mask |= select.EPOLLIN
if type == "all" or type == "control":
event_mask |= select.EPOLLPRI
if fd in self.cameras:
registered = fd in self.cameras
self.cameras[fd] = camera
if registered:
self.epoll.modify(fd, event_mask)
else:
self.epoll.register(fd, event_mask)
self.cameras[fd] = camera

def unregister(self, camera):
fd = camera.device.fileno()
camera = self.cameras.pop(fd, None)
if camera:
self.epoll.unregister(fd)

def loop(self):
errno = 0
Expand All @@ -104,27 +111,32 @@ def loop(self):
dispatcher = Dispatcher()


class MemMap(MemoryMap):
class QimageMemory(MemoryMap):
def open(self):
super().open()
self.opencv = False
self.data = None
self.opencv = None
format = self.format
if pixel_format := FORMAT_MAP.get(format.pixel_format):
if format.pixel_format in FORMAT_MAP:
self.data = bytearray(format.size)
self.qimage = QtGui.QImage(self.data, format.width, format.height, pixel_format)
elif format in OPENCV_FORMATS:
self.data = bytearray(format.width * format.height * 3)
self.qimage = QtGui.QImage(self.data, format.width, format.height, QtGui.QImage.Format.Format_RGB888)
self.opencv = True
else:
self.data = None
self.qimage = None
elif format.pixel_format in OPENCV_FORMATS:
self.data = bytearray(format.size)
self.opencv = create_opencv_buffer(format)

def raw_read(self, into=None):
frame = super().raw_read(self.data)
if self.data and frame.pixel_format in OPENCV_FORMATS:
frame_to_rgb24(frame, self.data)
frame.user_data = self.qimage
qimage = None
format = self.format
if self.data is not None:
if self.opencv is None:
data = self.data
qformat = FORMAT_MAP.get(format.pixel_format)
else:
data = self.opencv
opencv_frame_to_rgb24(frame, data)
qformat = QtGui.QImage.Format.Format_RGB888
qimage = QtGui.QImage(data, format.width, format.height, qformat)
frame.user_data = qimage
return frame


Expand All @@ -135,13 +147,31 @@ class QCamera(QtCore.QObject):
def __init__(self, device: Device):
super().__init__()
self.device = device
self.capture = VideoCapture(device, buffer_type=MemMap)
self.capture = VideoCapture(device, buffer_type=QimageMemory)
self._stop = False
self._stream = None
self._state = "stopped"
dispatcher.register(self, "control")
self.controls = {}

def __enter__(self):
self.device.open()
dispatcher.register(self, "control")
return self

def __exit__(self, *exc):
dispatcher.unregister(self)
self.device.close()

@classmethod
def from_id(cls, did: int, **kwargs):
device = Device.from_id(did, **kwargs)
return cls(device)

@classmethod
def from_path(cls, path, **kwargs):
device = Device(path, **kwargs)
return cls(device)

def handle_frame(self):
if self._stream is None:
return
Expand Down Expand Up @@ -210,7 +240,6 @@ def qcontrol(self, name_or_id: str | int):


class QVideoStream(QtCore.QObject):
qimage = None
imageChanged = QtCore.Signal(object)

def __init__(self, camera: QCamera | None = None):
Expand All @@ -219,8 +248,8 @@ def __init__(self, camera: QCamera | None = None):
self.set_camera(camera)

def on_frame(self, frame):
self.qimage = frame.user_data or frame_to_qimage(frame)
self.imageChanged.emit(self.qimage)
qimage = frame.user_data or frame_to_qimage(frame)
self.imageChanged.emit(qimage)

def set_camera(self, camera: QCamera | None = None):
if self.camera:
Expand Down Expand Up @@ -610,7 +639,15 @@ def to_qpixelformat(pixel_format: PixelFormat) -> QtGui.QPixelFormat | None:
}


def frame_to_rgb24(frame, into=None):
def create_opencv_buffer(format):
import numpy

depth = 3
dtype = numpy.ubyte
return numpy.empty((format.height, format.width, depth), dtype=dtype)


def opencv_frame_to_rgb24(frame, into=None):
import cv2

YUV_MAP = {
Expand All @@ -623,11 +660,12 @@ def frame_to_rgb24(frame, into=None):
}
data = frame.array
fmt = frame.pixel_format
if fmt in {PixelFormat.NV12, PixelFormat.YUV420}:
if fmt in {PixelFormat.NV12, PixelFormat.NV21, PixelFormat.YUV420}:
data.shape = frame.height * 3 // 2, frame.width, -1
else:
data.shape = frame.height, frame.width, -1
return cv2.cvtColor(data, YUV_MAP[fmt], dst=into)
result = cv2.cvtColor(data, YUV_MAP[fmt], dst=into)
return result


def frame_to_qimage(frame: Frame) -> QtGui.QImage | None:
Expand All @@ -637,7 +675,7 @@ def frame_to_qimage(frame: Frame) -> QtGui.QImage | None:
return QtGui.QImage.fromData(data, "JPG")

if frame.pixel_format in OPENCV_FORMATS:
data = frame_to_rgb24(frame)
data = opencv_frame_to_rgb24(frame)
fmt = QtGui.QImage.Format.Format_RGB888
else:
if (fmt := FORMAT_MAP.get(frame.pixel_format)) is None:
Expand Down Expand Up @@ -678,9 +716,6 @@ def draw_no_image(painter, width, height, line_width=4):
return draw_no_image_rect(painter, rect, line_width)


def draw_frame(widget, frame): ...


class BaseCameraControl:
def __init__(self, camera: QCamera | None = None):
self.camera = None
Expand Down Expand Up @@ -892,8 +927,7 @@ def stop():
fmt = "%(threadName)-10s %(asctime)-15s %(levelname)-5s %(name)s: %(message)s"
logging.basicConfig(level=args.log_level.upper(), format=fmt)
app = QtWidgets.QApplication([])
with Device.from_id(args.device, blocking=False) as device:
camera = QCamera(device)
with QCamera.from_id(args.device, blocking=False) as camera:
window = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(window)
video = QVideoWidget(camera)
Expand Down
10 changes: 5 additions & 5 deletions tests/test_video_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
except Exception:
qt = None

from linuxpy.video.device import Capability, Device, PixelFormat, VideoCapture
from linuxpy.video.device import Capability, PixelFormat, VideoCapture

qt_only = pytest.mark.skipif(qt is None, reason="qt not properly installed")
pytestmark = [qt_only, vivid_only]
Expand All @@ -18,14 +18,14 @@ def check_cannot(action, state):
getattr(qcamera, action)()
assert error.value.args[0] == f"Cannot {action} when camera is {state}"

with Device(VIVID_CAPTURE_DEVICE) as capture_dev:
with qt.QCamera.from_path(VIVID_CAPTURE_DEVICE) as qcamera:
capture_dev = qcamera.device
capture_dev.set_input(0)
width, height, pixel_format = 640, 480, PixelFormat.RGB24
capture = VideoCapture(capture_dev)
capture.set_format(width, height, pixel_format)
capture.set_fps(30)

qcamera = qt.QCamera(capture_dev)
assert qcamera.state() == "stopped"

check_cannot("pause", "stopped")
Expand Down Expand Up @@ -68,7 +68,8 @@ def check_cannot(action, state):

@pytest.mark.parametrize("pixel_format", [PixelFormat.RGB24, PixelFormat.RGB32, PixelFormat.ARGB32, PixelFormat.YUYV])
def test_qvideo_widget(qtbot, pixel_format):
with Device(VIVID_CAPTURE_DEVICE) as capture_dev:
with qt.QCamera.from_path(VIVID_CAPTURE_DEVICE) as qcamera:
capture_dev = qcamera.device
capture_dev.set_input(0)
width, height = 640, 480
capture = VideoCapture(capture_dev)
Expand All @@ -92,7 +93,6 @@ def test_qvideo_widget(qtbot, pixel_format):
assert status.args[0] == "running"
assert qcamera.state() == "running"
check_frame(frames.args[0], width, height, pixel_format, Capability.STREAMING)
assert widget.video.frame is frames.args[0]
assert play_button.toolTip() == "Camera is running. Press to stop it"

qtbot.waitUntil(lambda: widget.video.qimage is not None)
Expand Down
38 changes: 38 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.