Coverage for linuxpy/video/qt.py: 0%
280 statements
« prev ^ index » next coverage.py v7.6.8, created at 2025-05-27 13:54 +0200
« prev ^ index » next coverage.py v7.6.8, created at 2025-05-27 13:54 +0200
1#
2# This file is part of the linuxpy project
3#
4# Copyright (c) 2023 Tiago Coutinho
5# Distributed under the GPLv3 license. See LICENSE for more info.
7"""
8Qt helpers for V4L2 (Video 4 Linux 2) subsystem.
10You'll need to install linuxpy qt optional dependencies (ex: `$pip install linuxpy[qt]`)
11"""
13import logging
15from qtpy import QtCore, QtGui, QtWidgets
17from linuxpy.video.device import Device, Frame, PixelFormat, VideoCapture
20class QCamera(QtCore.QObject):
21 frameChanged = QtCore.Signal(object)
22 stateChanged = QtCore.Signal(str)
24 def __init__(self, device: Device):
25 super().__init__()
26 self.device = device
27 self.capture = VideoCapture(device)
28 self._stop = False
29 self._stream = None
30 self._notifier = None
31 self._state = "stopped"
33 def on_frame(self):
34 frame = next(self._stream)
35 self.frameChanged.emit(frame)
37 def setState(self, state):
38 self._state = state
39 self.stateChanged.emit(state)
41 def state(self):
42 return self._state
44 def start(self):
45 if self._state != "stopped":
46 return
47 self.setState("running")
48 self.device.open()
49 self.capture.open()
50 self._stream = iter(self.capture)
51 self._notifier = QtCore.QSocketNotifier(self.device.fileno(), QtCore.QSocketNotifier.Type.Read)
52 self._notifier.activated.connect(self.on_frame)
54 def pause(self):
55 if self._state != "running":
56 return
57 self._notifier.setEnabled(False)
58 self.setState("paused")
60 def resume(self):
61 if self._state != "paused":
62 return
63 self._notifier.setEnabled(True)
64 self.setState("running")
66 def stop(self):
67 if self._notifier is not None:
68 self._notifier.setEnabled(False)
69 self._notifier = None
70 if self._stream is not None:
71 self._stream.close()
72 self._stream = None
73 self.capture.close()
74 self.device.close()
75 self._notifier = None
76 self.setState("stopped")
79def to_qpixelformat(pixel_format: PixelFormat) -> QtGui.QPixelFormat | None:
80 if pixel_format == PixelFormat.YUYV:
81 return QtGui.qPixelFormatYuv(QtGui.QPixelFormat.YUVLayout.YUYV)
84def frame_to_qimage(frame: Frame) -> QtGui.QImage:
85 """Translates a Frame to a QImage"""
86 if frame.pixel_format == PixelFormat.MJPEG:
87 return QtGui.QImage.fromData(frame.data, b"JPG")
88 fmt = QtGui.QImage.Format.Format_BGR888
89 if frame.pixel_format == PixelFormat.RGB24:
90 fmt = QtGui.QImage.Format.Format_RGB888
91 elif frame.pixel_format == PixelFormat.RGB32:
92 fmt = QtGui.QImage.Format.Format_RGB32
93 elif frame.pixel_format == PixelFormat.ARGB32:
94 fmt = QtGui.QImage.Format.Format_ARGB32
95 elif frame.pixel_format == PixelFormat.YUYV:
96 import cv2
98 data = frame.array
99 data.shape = frame.height, frame.width, -1
100 data = cv2.cvtColor(data, cv2.COLOR_YUV2BGR_YUYV)
101 return QtGui.QImage(frame.data, frame.width, frame.height, fmt)
104def frame_to_qpixmap(frame: Frame) -> QtGui.QPixmap:
105 if frame.pixel_format == PixelFormat.MJPEG:
106 pixmap = QtGui.QPixmap(frame.width, frame.height)
107 pixmap.loadFromData(frame.data, b"JPG")
108 return pixmap
109 qimage = frame_to_qimage(frame)
110 return QtGui.QPixmap.fromImage(qimage)
113def draw_frame(paint_device, width, height, line_width=4, color="red"):
114 if width is None:
115 width = paint_device.width()
116 if height is None:
117 height = paint_device.height()
118 half_line_width = line_width // 2
119 pen = QtGui.QPen(QtGui.QColor(color), line_width)
120 painter = QtGui.QPainter(paint_device)
121 painter.setPen(pen)
122 painter.setBrush(QtCore.Qt.NoBrush)
123 painter.drawRect(half_line_width, half_line_width, width - line_width, height - line_width)
126def draw_no_image(paint_device, width=None, height=None, line_width=4):
127 if width is None:
128 width = paint_device.width()
129 if height is None:
130 height = paint_device.height()
131 color = QtGui.QColor(255, 0, 0, 100)
132 pen = QtGui.QPen(color, line_width)
133 painter = QtGui.QPainter(paint_device)
134 painter.setPen(pen)
135 painter.setBrush(QtCore.Qt.NoBrush)
136 painter.drawLines(
137 (
138 QtCore.QLine(0, height, width, 0),
139 QtCore.QLine(0, 0, width, height),
140 )
141 )
142 half_line_width = line_width // 2
143 painter.drawRect(half_line_width, half_line_width, width - line_width, height - line_width)
146class QVideoInfo(QtWidgets.QWidget):
147 def __init__(self):
148 super().__init__()
149 self._init()
151 def _init(self):
152 layout = QtWidgets.QFormLayout()
153 self.setLayout(layout)
156class BaseCameraControl:
157 def __init__(self, camera: QCamera | None = None):
158 self.camera = None
159 self.set_camera(camera)
161 def set_camera(self, camera: QCamera | None):
162 if self.camera:
163 self.camera.stateChanged.disconnect(self.on_camera_state_changed)
164 self.camera = camera
165 if self.camera:
166 self.camera.stateChanged.connect(self.on_camera_state_changed)
167 state = self.camera.state()
168 else:
169 state = None
170 self.on_camera_state_changed(state)
172 def on_camera_state_changed(self, state):
173 pass
176class PlayButtonControl(BaseCameraControl):
177 play_icon = "media-playback-start"
178 stop_icon = "media-playback-stop"
180 def __init__(self, button, camera: QCamera | None = None):
181 self.button = button
182 self.button.clicked.connect(self.on_click)
183 super().__init__(camera)
185 def on_camera_state_changed(self, state):
186 enabled = True
187 icon = self.stop_icon
188 if state is None:
189 enabled = False
190 tip = "No camera attached to this button"
191 elif state == "stopped":
192 icon = self.play_icon
193 tip = "Camera is stopped. Press to start rolling"
194 else:
195 tip = f"Camera is {state}. Press to stop it"
196 self.button.setEnabled(enabled)
197 self.button.setIcon(QtGui.QIcon.fromTheme(icon))
198 self.button.setToolTip(tip)
200 def on_click(self):
201 if self.camera:
202 if self.camera.state() == "stopped":
203 self.camera.start()
204 else:
205 self.camera.stop()
208class PauseButtonControl(BaseCameraControl):
209 play_icon = "media-playback-start"
210 pause_icon = "media-playback-pause"
212 def __init__(self, button, camera: QCamera | None = None):
213 self.button = button
214 self.button.clicked.connect(self.on_click)
215 super().__init__(camera)
217 def on_camera_state_changed(self, state):
218 if state is None:
219 tip = "No camera attached to this button"
220 elif state == "stopped":
221 tip = "Camera is stopped"
222 elif state == "paused":
223 tip = "Camera is paused. Press to resume"
224 else:
225 tip = f"Camera is {state}. Press to pause"
226 enabled = state not in {None, "stopped"}
227 icon = self.play_icon if state == "paused" else self.pause_icon
228 self.button.setEnabled(enabled)
229 self.button.setIcon(QtGui.QIcon.fromTheme(icon))
230 self.button.setToolTip(tip)
232 def on_click(self):
233 if self.camera is None:
234 return
235 if self.camera.state() == "running":
236 self.camera.pause()
237 else:
238 self.camera.resume()
241class StateControl(BaseCameraControl):
242 def __init__(self, label, camera: QCamera | None = None):
243 self.label = label
244 super().__init__(camera)
246 def on_camera_state_changed(self, state):
247 if state is None:
248 state = "---"
249 self.label.setText(state)
252class QVideoControls(QtWidgets.QWidget):
253 def __init__(self, camera: QCamera | None = None):
254 super().__init__()
255 self.camera = None
256 self._init()
257 self.set_camera(camera)
259 def _init(self):
260 layout = QtWidgets.QHBoxLayout()
261 layout.setContentsMargins(0, 0, 0, 0)
262 self.setLayout(layout)
263 state_label = QtWidgets.QLabel("")
264 play_button = QtWidgets.QToolButton()
265 pause_button = QtWidgets.QToolButton()
266 self.camera_label = QtWidgets.QLabel("")
267 layout.addWidget(self.camera_label)
268 layout.addStretch(1)
269 layout.addWidget(state_label)
270 layout.addWidget(play_button)
271 layout.addWidget(pause_button)
272 self.state_control = StateControl(state_label)
273 self.play_control = PlayButtonControl(play_button)
274 self.pause_control = PauseButtonControl(pause_button)
276 def set_camera(self, camera: QCamera | None = None):
277 self.state_control.set_camera(camera)
278 self.play_control.set_camera(camera)
279 self.pause_control.set_camera(camera)
280 if camera is None:
281 name = "---"
282 else:
283 name = camera.device.filename.stem
284 self.camera_label.setText(name)
285 self.camera = camera
288class QVideo(QtWidgets.QWidget):
289 frame = None
290 image = None
292 def __init__(self, camera: QCamera | None = None):
293 super().__init__()
294 self.camera = None
295 self.set_camera(camera)
297 def set_camera(self, camera: QCamera | None = None):
298 if self.camera:
299 self.camera.frameChanged.disconnect(self.on_frame_changed)
300 self.camera = camera
301 if self.camera:
302 self.camera.frameChanged.connect(self.on_frame_changed)
304 def on_frame_changed(self, frame):
305 self.frame = frame
306 self.pixmap = None
307 self.update()
309 def paintEvent(self, _):
310 frame = self.frame
311 if frame is None:
312 draw_no_image(self)
313 return
314 if self.pixmap is None:
315 self.pixmap = frame_to_qpixmap(frame)
316 if self.pixmap is not None:
317 width, height = self.width(), self.height()
318 scaled_pixmap = self.pixmap.scaled(width, height, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
319 pix_width, pix_height = scaled_pixmap.width(), scaled_pixmap.height()
320 x, y = 0, 0
321 if width > pix_width:
322 x = int((width - pix_width) / 2)
323 if height > pix_height:
324 y = int((height - pix_height) / 2)
325 painter = QtGui.QPainter(self)
326 painter.drawPixmap(QtCore.QPoint(x, y), scaled_pixmap)
328 def minimumSizeHint(self):
329 return QtCore.QSize(160, 120)
332class QVideoWidget(QtWidgets.QWidget):
333 def __init__(self, camera=None):
334 super().__init__()
335 layout = QtWidgets.QVBoxLayout(self)
336 self.video = QVideo(camera)
337 self.controls = QVideoControls(camera)
338 layout.addWidget(self.video)
339 layout.addWidget(self.controls)
340 layout.setStretchFactor(self.video, 1)
342 def set_camera(self, camera: QCamera | None = None):
343 self.video.set_camera(camera)
344 self.controls.set_camera(camera)
347def main():
348 import argparse
350 parser = argparse.ArgumentParser()
351 parser.add_argument("--log-level", choices=["debug", "info", "warning", "error"], default="info")
352 parser.add_argument("device", type=int)
353 args = parser.parse_args()
354 fmt = "%(threadName)-10s %(asctime)-15s %(levelname)-5s %(name)s: %(message)s"
355 logging.basicConfig(level=args.log_level.upper(), format=fmt)
356 app = QtWidgets.QApplication([])
357 device = Device.from_id(args.device)
358 camera = QCamera(device)
359 widget = QVideoWidget(camera)
360 app.aboutToQuit.connect(camera.stop)
361 widget.show()
362 app.exec()
365if __name__ == "__main__":
366 main()