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

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. 

6 

7""" 

8Qt helpers for V4L2 (Video 4 Linux 2) subsystem. 

9 

10You'll need to install linuxpy qt optional dependencies (ex: `$pip install linuxpy[qt]`) 

11""" 

12 

13import logging 

14 

15from qtpy import QtCore, QtGui, QtWidgets 

16 

17from linuxpy.video.device import Device, Frame, PixelFormat, VideoCapture 

18 

19 

20class QCamera(QtCore.QObject): 

21 frameChanged = QtCore.Signal(object) 

22 stateChanged = QtCore.Signal(str) 

23 

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" 

32 

33 def on_frame(self): 

34 frame = next(self._stream) 

35 self.frameChanged.emit(frame) 

36 

37 def setState(self, state): 

38 self._state = state 

39 self.stateChanged.emit(state) 

40 

41 def state(self): 

42 return self._state 

43 

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) 

53 

54 def pause(self): 

55 if self._state != "running": 

56 return 

57 self._notifier.setEnabled(False) 

58 self.setState("paused") 

59 

60 def resume(self): 

61 if self._state != "paused": 

62 return 

63 self._notifier.setEnabled(True) 

64 self.setState("running") 

65 

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") 

77 

78 

79def to_qpixelformat(pixel_format: PixelFormat) -> QtGui.QPixelFormat | None: 

80 if pixel_format == PixelFormat.YUYV: 

81 return QtGui.qPixelFormatYuv(QtGui.QPixelFormat.YUVLayout.YUYV) 

82 

83 

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 

97 

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) 

102 

103 

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) 

111 

112 

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) 

124 

125 

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) 

144 

145 

146class QVideoInfo(QtWidgets.QWidget): 

147 def __init__(self): 

148 super().__init__() 

149 self._init() 

150 

151 def _init(self): 

152 layout = QtWidgets.QFormLayout() 

153 self.setLayout(layout) 

154 

155 

156class BaseCameraControl: 

157 def __init__(self, camera: QCamera | None = None): 

158 self.camera = None 

159 self.set_camera(camera) 

160 

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) 

171 

172 def on_camera_state_changed(self, state): 

173 pass 

174 

175 

176class PlayButtonControl(BaseCameraControl): 

177 play_icon = "media-playback-start" 

178 stop_icon = "media-playback-stop" 

179 

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) 

184 

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) 

199 

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() 

206 

207 

208class PauseButtonControl(BaseCameraControl): 

209 play_icon = "media-playback-start" 

210 pause_icon = "media-playback-pause" 

211 

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) 

216 

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) 

231 

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() 

239 

240 

241class StateControl(BaseCameraControl): 

242 def __init__(self, label, camera: QCamera | None = None): 

243 self.label = label 

244 super().__init__(camera) 

245 

246 def on_camera_state_changed(self, state): 

247 if state is None: 

248 state = "---" 

249 self.label.setText(state) 

250 

251 

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) 

258 

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) 

275 

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 

286 

287 

288class QVideo(QtWidgets.QWidget): 

289 frame = None 

290 image = None 

291 

292 def __init__(self, camera: QCamera | None = None): 

293 super().__init__() 

294 self.camera = None 

295 self.set_camera(camera) 

296 

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) 

303 

304 def on_frame_changed(self, frame): 

305 self.frame = frame 

306 self.pixmap = None 

307 self.update() 

308 

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) 

327 

328 def minimumSizeHint(self): 

329 return QtCore.QSize(160, 120) 

330 

331 

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) 

341 

342 def set_camera(self, camera: QCamera | None = None): 

343 self.video.set_camera(camera) 

344 self.controls.set_camera(camera) 

345 

346 

347def main(): 

348 import argparse 

349 

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() 

363 

364 

365if __name__ == "__main__": 

366 main()