CVBpy 14.1
gevserver/QmlGevServer
1# ---------------------------------------------------------------------------
2# The QmlGevServer example starts a QML GevServer which can load a device
3# and stream the device image.
4# ---------------------------------------------------------------------------
5
6import os, sys
7
8import cvb
9import cvb.gevserver as gs
10import cvb.ui
11
12if sys.version_info >= (3, 11):
13 from PySide6.QtWidgets import QApplication
14 from PySide6.QtQuick import QQuickView
15 from PySide6.QtCore import QObject, Signal, Slot, Property, QUrl
16 from PySide6.QtGui import QIcon
17else:
18 from PySide2.QtWidgets import QApplication
19 from PySide2.QtQuick import QQuickView
20 from PySide2.QtCore import QObject, Signal, Slot, Property, QUrl
21 from PySide2.QtGui import QIcon
22
23class StreamProcessingHandler(cvb.SingleStreamHandler):
24 def __init__(self, stream):
25 super().__init__(stream)
26
27 def handle_async_wait_result(self, image, status):
28 if status is cvb.WaitStatus.Ok:
29 print("New image arrived")
30 # display image or
31 if BackEnd.grab_image or BackEnd.single_shot:
32 BackEnd.single_shot = False
33 BackEnd.image_controller.refresh(image)
34
35 if BackEnd.has_ring_buffer:
36 image.unlock()
37 # send image
38 elif BackEnd.server_started and (not BackEnd.server_single_shot_send_enabled or BackEnd.server_single_shot_send) and BackEnd.server.stream.is_running:
39 try:
40 print('Sending image')
41 BackEnd.server_single_shot_send = False
42 if BackEnd.has_ring_buffer:
43 BackEnd.buffer_index = image.buffer_index
44 BackEnd.img_list[image.buffer_index] = image
45
46 def image_release(img):
47 img.unlock()
48 BackEnd.img_list[BackEnd.buffer_index].unlock()
49 print('Unlocked buffer at index ' + str(img.buffer_index))
50
51 BackEnd.server.stream.send(image, image_release)
52 BackEnd.image_controller.refresh(BackEnd.img_list[BackEnd.buffer_index])
53 else:
54 BackEnd.server.stream.send(image)
55 BackEnd.image_controller.refresh(image)
56 except:
57 print('Failed to send image')
58 elif BackEnd.has_ring_buffer:
59 print("Unlock unused image")
60 image.unlock()
61
62
63class BackEnd(QObject):
64 connection_list_changed = Signal()
65 server_started = False
66 server_single_shot_send_enabled = False
67 server_single_shot_send = False
68 single_shot = False
69 server = None
70 grab_image = False
71 has_ring_buffer = False
72 img_list = list()
73 buffer_index = 0
74 # create an image controller to interact with the UI
75 image_controller = cvb.ui.ImageController()
76
77 def __init__(self):
78 QObject.__init__(self)
79 self.start_in_progress = False
80 self.window_state_reg_node = None
81 self.event_cookie = None
82
83 # load the device
84 self.device = cvb.DeviceFactory.open(
85 os.path.join(cvb.install_path(), 'drivers', 'CVMock.vin'),
86 cvb.AcquisitionStack.Vin)
87 self.stream = self.device.stream()
88
89 BackEnd.has_ring_buffer = bool(self.stream.ring_buffer)
90 if BackEnd.has_ring_buffer:
91 self.stream.ring_buffer.lock_mode = cvb.RingBufferLockMode.On
92 BackEnd.img_list = [None] * self.stream.ring_buffer.count
93
94 # use a single stream handler to setup an acquisition thread and acquire images
95 self.handler = StreamProcessingHandler(self.stream)
96 self.view = view
97 self.view.rootContext().setContextProperty('mainImage', BackEnd.image_controller)
98 # register the device image with UI controller to trigger automatic refreshes
99 self.stream.get_snapshot()
100 BackEnd.image_controller.refresh(self.device.device_image)
101 self.handler.run()
102
103 # fill combo box with all available connections
104 self.connections = gs.LogicalNetworkInterface.get_all_available()
105 self.m_connection_list = list()
106 for connection in self.connections:
107 self.m_connection_list.append(connection.ip_address() + ' (' + connection.ipv4_mask() + ')')
108 self.connection_list_changed.emit()
109
110 def __enter__(self):
111 return self
112
113 def __exit__(self, exc_type, exc_val, exc_tb):
114 if self.handler.is_active:
115 self.handler.finish()
116 if BackEnd.server:
117 BackEnd.server = None
118
119 @Property('QVariantList', notify=connection_list_changed)
120 def connection_list(self):
121 return self.m_connection_list
122
123 @Property('QUrl')
124 def driver_path(self):
125 return QUrl.fromLocalFile(cvb.expand_path('%CVB%drivers'))
126
127 @Property('QUrl')
128 def desktop_path(self):
129 return QUrl.fromLocalFile(os.path.expanduser("~/Desktop"))
130
131 @Slot(bool)
132 def switch_grab(self, switched):
133 if switched and not self.handler.is_active:
134 self.handler.run()
135 BackEnd.grab_image = switched
136
137 def reset_ui(self):
138 print("Resetting ui")
139 if BackEnd.grab_image:
140 print('Resetting grab switch')
141 BackEnd.grab_image = False
142 switch = self.view.findChild(QObject, 'switchGrab')
143 switch.setProperty('checked', False)
144
145 def stop_grab(self):
146 print('Stopping grab')
147 self.reset_ui()
148 if self.stream.is_running:
149 self.handler.try_finish()
150 print('Grab stopped')
151
152 @Slot(str)
153 def load_device(self, path):
154 print('Loading device from ' + path)
155 self.stop_grab()
156 try:
157 tmp_device = cvb.DeviceFactory.open(path, cvb.AcquisitionStack.Vin)
158 self.device = tmp_device
159 self.stream = self.device.stream()
160 self.handler = StreamProcessingHandler(self.stream)
161
162 self.stream.get_snapshot()
163 BackEnd.has_ring_buffer = bool(self.stream.ring_buffer)
164 if BackEnd.has_ring_buffer:
165 self.stream.ring_buffer.lock_mode = cvb.RingBufferLockMode.On
166 BackEnd.img_list = [None] * self.stream.ring_buffer.count
167 BackEnd.image_controller.refresh(self.device.device_image)
168 self.handler.run()
169 except:
170 print('Failed to load new device')
171
172 @Slot(str)
173 def save_image(self, path):
174 print('Saving image to ' + path)
175 self.device.device_image.save(path)
176
177 @Slot()
178 def btn_single_shot(self):
179 print('Single Shot')
180 self.reset_ui()
181 if not self.handler.is_active:
182 self.handler.run()
183 BackEnd.single_shot = True
184
185 @Slot(bool)
186 def switch_enable_single_shot_send(self, enabled):
187 BackEnd.server_single_shot_send_enabled = enabled
188
189 @Slot()
190 def btn_single_shot_send(self):
191 if BackEnd.server.stream.is_running:
192 print('Send single shot')
193 BackEnd.server_single_shot_send = True
194 else:
195 print('Acquisition not enabled on server')
196
197 @Slot(int, int)
198 def start_server(self, connection_index, driver_type_index):
199 print('connection_index', connection_index, 'driver_type_index', driver_type_index)
200 # check for recursions
201 if self.start_in_progress:
202 return
203
204 self.start_in_progress = True
205
206 try:
207 if BackEnd.server_started is False:
208 self.reset_ui()
209
210 # create server
211 BackEnd.server = gs.Server.create_with_const_size(self.device.device_image.size,
212 self.device.device_image.color_model,
213 self.device.device_image.planes[0].data_type,
214 driver_type_index)
215
216 self.add_genicam_features()
217
218 BackEnd.server.user_version = 'Python GevServer'
219 BackEnd.server.node_map.xml_file_version = cvb.GenApiVersion(1, 0, 0)
220
221 if(BackEnd.has_ring_buffer):
222 BackEnd.server.stream.resend_buffers_count = 2
223 else:
224 BackEnd.server.stream.resend_buffers_count = 0
225
226 # start server
227 BackEnd.server.start(self.connections[connection_index].ip_address())
228
229 # start acquisition
230 BackEnd.server_started = True
231 print('Started server')
232 else:
233 BackEnd.server.stop()
234 BackEnd.server_started = False
235 print('Server stopped')
236
237 except:
238 print('Failed to start/stop the server')
239 self.start_in_progress = False
240
241 def on_window_size_changed(self, valueNode):
242 print('Window size change callback called')
243 val = self.window_state_reg_node.value
244 if val == 0:
245 self.view.showMinimized()
246 elif val == 1:
247 self.view.showNormal()
248 elif val == 2:
249 view.showMaximized()
250
251
252 def add_genicam_features(self):
253 cat_node = gs.CategoryNode.create('Cust::CustomFeatures')
254 BackEnd.server.node_map.add_node(cat_node)
255 cat_node.display_name = 'Custom Features'
256 cat_node.tool_tip = 'Contains all application defined features'
257 root_node = BackEnd.server.node_map['Root']
258 root_node.add(cat_node, gs.NodeList.Child)
259
260 self.window_state_reg_node = gs.Int32RegNode.create('Cust::WindowStateReg')
261 BackEnd.server.node_map.add_node(self.window_state_reg_node)
262 self.window_state_reg_node.visibility = cvb.Visibility.Invisible
263 # no cache because of polling
264 self.window_state_reg_node.cache_mode = cvb.CacheMode.NoCache
265 # polling time in ms
266 self.window_state_reg_node.polling_time = 333
267 self.window_state_reg_node.value = 1 # view in normal mode
268 self.event_cookie = self.window_state_reg_node.register_event_written_updated(self.on_window_size_changed)
269
270 enumeration_node = gs.EnumerationNode.create('Cust::WindowState')
271 BackEnd.server.node_map.add_node(enumeration_node)
272 enumeration_node.display_name = 'Window State'
273 enumeration_node.tool_tip = 'Current window state of server application'
274 enumeration_node.value_config_as_node = self.window_state_reg_node
275
276 minimized_node = gs.EnumEntryNode.create('Cust::Minimized')
277 minimized_node.numeric_value = 0
278 enumeration_node.add(minimized_node, gs.NodeList.Child)
279
280 normal_node = gs.EnumEntryNode.create('Cust::Normal')
281 normal_node.numeric_value = 1 # must be ascending entry
282 enumeration_node.add(normal_node, gs.NodeList.Child)
283
284 max_node = gs.EnumEntryNode.create('Cust::Maximized')
285 max_node.numeric_value = 2
286 enumeration_node.add(max_node, gs.NodeList.Child)
287
288 cat_node.add(enumeration_node, gs.NodeList.Child)
289
290
291if __name__ == '__main__':
292 sys.argv += ['--style', 'Windows']
293 app = QApplication(sys.argv)
294 app.setOrganizationName('STEMMER IMAGING')
295 app.setOrganizationDomain('https://www.stemmer-imaging.com/')
296 app.setApplicationName('GevServer Python tutorial')
297
298 # tell Windows the correct AppUserModelID for this process (shows icon in the taskbar)
299 if sys.platform == 'win32':
300 import ctypes
301 myappid = u'stemmerimaging.commonvisionblox.pygevserver.0'
302 ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
303
304 app.setWindowIcon(QIcon('Tutorial-Python_32x32.png'))
305
306 # register the display component with QML
308
309 # setup the QML UI
310 view = QQuickView()
311 view.setResizeMode(QQuickView.SizeRootObjectToView)
312
313 # create backend object
314 with BackEnd() as backEnd:
315
316 # register it to the context of QML
317 view.rootContext().setContextProperty('backEnd', backEnd)
318 # load the qml file into the engine
319 view.setSource(QUrl('main.qml'))
320 # call single shot to update the UI
321 backEnd.btn_single_shot()
322
323 view.resize(640, 390)
324 view.show()
325
326 # start the UI event handler
327 app.exec_()
328
Union[cvb.GenICamDevice, cvb.VinDevice, cvb.EmuDevice, cvb.VideoDevice, cvb.NonStreamingDevice] open(str provider, int acquisition_stack=cvb.AcquisitionStack.PreferVin)
Opens a device with the given provider and acquisition stack.
Definition: __init__.py:1570
Version information for GenICam related objects.
Definition: __init__.py:2008
Handler object for a single stream.
Definition: __init__.py:5398
Controller object for the QML image view item.
Definition: __init__.py:14
None register(cls, str uri="CvbQuick", int version_major=1, int version_minor=0, str qml_name="ImageView")
Convenience method to register this type or a derived type in QML.
Definition: __init__.py:196
Common Vision Blox GevServer module for Python.
Definition: __init__.py:1
Common Vision Blox UI module for Python.
Definition: __init__.py:1
str install_path()
Directory Common Vision Blox has been installed to.
Definition: __init__.py:8257
str expand_path(str path)
Expands a path containing an environment variable.
Definition: __init__.py:8141
1import QtQuick 2.0
2import CvbQuick 1.0 as CvbQuick
3import QtQuick.Controls 2.12
4import QtQuick.Layouts 1.12
5import QtQuick.Dialogs 1.3
6import QtQuick.Controls.Universal 2.4
7import QtQuick.Window 2.3
8
9
10GroupBox {
11 id: gridBox
12 width: 640
13 height: 390
14 Layout.fillWidth: true
15 Layout.fillHeight: true
16
17 property var loadFile: true
18 property var enableSingleShotSend: false
19
20 Universal.theme: Universal.System
21
22 background: Rectangle {
23 width: parent.width
24 height: parent.height
25 color: Universal.background
26 }
27
28 FileDialog {
29 id: fileDialog
30 modality: Qt.NonModal
31 title: loadFile ? "Select an image or driver to load" : "Save image"
32 selectExisting: loadFile
33 selectFolder: false
34 nameFilters: ["Drivers (*.vin *.emu)", "Image files (*png *.bmp)", "All files (*)"]
35 selectedNameFilter: loadFile ? "Drivers" : "Image files"
36 sidebarVisible: true
37 onAccepted: {
38 var path = fileUrl.toString();
39 // remove prefixed "file:///"
40 path = path.replace(/^(file:\/{3})/,"");
41 // unescape html codes like '%23' for '#'
42 var cleanPath = decodeURIComponent(path);
43
44 loadFile ? backEnd.load_device(path) : backEnd.save_image(path)
45 }
46 }
47
48 GridLayout {
49 id: gridLayout
50 rows: 4
51 flow: GridLayout.TopToBottom
52 anchors.fill: parent
53
54 ComboBox
55 {
56 id: connectionCombo
57 objectName: "connectionCombo"
58 enabled: btnStartServer.m_server_disabled
59 Layout.fillWidth: true
60 Layout.columnSpan: 2
61 model: backEnd.connection_list
62 }
63
64 Rectangle {
65 id: page
66 Layout.rowSpan: 3
67 Layout.columnSpan: 3
68 Layout.fillHeight: true
69 Layout.fillWidth: true
70 CvbQuick.ImageView
71 {
72 id: view
73 image: mainImage
74 anchors.fill: parent
75 }
76 }
77
78 ComboBox
79 {
80 id: driverTypeCombo
81 enabled: btnStartServer.m_server_disabled
82 Layout.fillWidth: true
83 model:
84 (Qt.platform.os == "windows") ?
85 [
86 qsTr("Socket Driver (loopback)"),
87 qsTr("Filter Driver (default)")
88 ]
89 :
90 [
91 qsTr("Socket Driver")
92 ]
93 }
94
95 Button
96 {
97 objectName: "btnStartServer"
98 id: btnStartServer
99 implicitWidth: btnLoadDevice.width
100 property var m_server_disabled: true
101 text: m_server_disabled ? qsTr("Start Server") : qsTr("Stop Server")
102 onClicked: {
103 m_server_disabled = m_server_disabled ? false : true
104 backEnd.start_server(connectionCombo.currentIndex, driverTypeCombo.currentIndex)
105 }
106 }
107
108 Column
109 {
110 spacing: 5
111
112 Button
113 {
114 objectName: "btnLoadDevice"
115 id: btnLoadDevice
116 enabled: btnStartServer.m_server_disabled
117 text: qsTr("Load Device")
118 onClicked:
119 {
120 loadFile = true
121 fileDialog.folder = backEnd.driver_path
122 fileDialog.open()
123 }
124
125 }
126
127 Button
128 {
129 objectName: "btnSaveImg"
130 id: btnSaveImg
131 implicitWidth: btnLoadDevice.width
132 text: qsTr("Save Image")
133 onClicked:
134 {
135 loadFile = false
136 fileDialog.folder = backEnd.desktop_path
137 fileDialog.open()
138 }
139 }
140
141 Button
142 {
143 objectName: "btnSingleShot"
144 id: btnSingleShot
145 implicitWidth: btnLoadDevice.width
146 width: btnSaveImg.width
147 enabled: btnStartServer.m_server_disabled
148 text: qsTr("Single Shot")
149 onClicked: backEnd.btn_single_shot()
150 }
151
152 Switch
153 {
154 objectName: "switchEnableSingleShotSend"
155 id: switchEnableSingleShotSend
156 text: qsTr("Enable Single Shot Send")
157 checked: false
158 enabled: !btnStartServer.m_server_disabled
159 onCheckedChanged:
160 {
161 enableSingleShotSend = enableSingleShotSend ? false : true
162 backEnd.switch_enable_single_shot_send(checked)
163 }
164 }
165
166 Button
167 {
168 objectName: "btnSingleShotSend"
169 id: btnSingleShotSend
170 implicitWidth: btnLoadDevice.width
171 enabled: !btnStartServer.m_server_disabled && enableSingleShotSend
172 text: qsTr("Single Shot Send")
173 onClicked: backEnd.btn_single_shot_send()
174 }
175 }
176
177 Switch
178 {
179 objectName: "switchGrab"
180 id: switchGrab
181 text: qsTr("Grab")
182 checked: false
183 enabled: btnStartServer.m_server_disabled
184 onCheckedChanged: backEnd.switch_grab(checked)
185 }
186 }
187}