cyberglove_tweaker.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # Copyright 2018 Shadow Robot Company Ltd.
4 #
5 # This program is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation version 2 of the License.
8 #
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 # more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 
18 
19 import rospy
20 import rospkg
21 
22 
23 from qt_gui.plugin import Plugin
24 import QtCore
25 from QtCore import Qt, QEvent, QObject, pyqtSignal
26 import QtGui
27 from QtGui import *
28 import QtWidgets
29 from QtWidgets import *
30 from math import degrees
31 from copy import deepcopy
32 
33 from cyberglove_calibrer import *
34 from cyberglove_mapper import *
35 
36 from sensor_msgs.msg import JointState
37 from std_srvs.srv import Empty
38 
39 import sip
40 
41 rootPath = os.path.join(
42  rospkg.RosPack().get_path('sr_gui_cyberglove_calibrator'))
43 
44 
45 class QHLine(QFrame):
46  def __init__(self):
47  super(QHLine, self).__init__()
48  self.setFrameShape(QFrame.HLine)
49  self.setFrameShadow(QFrame.Sunken)
50 
51 
52 class QVLine(QFrame):
53  def __init__(self):
54  super(QVLine, self).__init__()
55  self.setFrameShape(QFrame.VLine)
56  self.setFrameShadow(QFrame.Sunken)
57 
58 
59 class SrGuiCyberglovePointTweaker(QtWidgets.QWidget):
60  """
61  Adjustment for one calibration point
62  """
63  def _get_button(self, text, amount):
64  button = QPushButton(text)
65  button.clicked.connect(lambda: self._change_raw(amount))
66  button.setMaximumSize(40, 40)
67  return button
68 
69  def __init__(self, index, raw, calibrated, button_callback=lambda x, y: x):
70  super(SrGuiCyberglovePointTweaker, self).__init__()
71  self.setLayout(QGridLayout())
72  self.layout().setVerticalSpacing(25)
73 
74  self._raw_value_label = QLabel()
75  self._calibrated_value_label = QLabel()
76 
77  up1 = self._get_button("+++", 0.1)
78  up2 = self._get_button("++", 0.01)
79  up3 = self._get_button("+", 0.001)
80 
81  down1 = self._get_button("- - -", -0.1)
82  down2 = self._get_button("- -", -0.01)
83  down3 = self._get_button("-", -0.001)
84 
85  self.layout().addWidget(self._calibrated_value_label, 0, 0)
86  self.layout().addWidget(self._raw_value_label, 1, 0)
87 
88  self.layout().addWidget(up1, 0, 1)
89  self.layout().addWidget(up2, 0, 2)
90  self.layout().addWidget(up3, 0, 3)
91  self.layout().addWidget(down1, 1, 1)
92  self.layout().addWidget(down2, 1, 2)
93  self.layout().addWidget(down3, 1, 3)
94 
95  self._button_callback = button_callback
96 
97  self._set_raw_value(raw)
98  self._set_calibrated_value(calibrated)
99  self._index = index
100 
101  def _change_raw(self, amount):
102  self._set_raw_value(self._raw_value + amount)
103  self._button_callback(self._index, self._raw_value)
104 
105  def _set_calibrated_value(self, value):
106  self._calibrated_value = value
107  self._calibrated_value_label.setText("%.4f (Cal)" % self._calibrated_value)
108 
109  def _set_raw_value(self, value):
110  self._raw_value = value
111  self._raw_value_label.setText("%.4f (Raw)" % self._raw_value)
112 
113 
114 class SrGuiCybergloveJointTweaker(QtWidgets.QWidget):
115  """
116  Calibrator for one joint, expecting linear calibrations between two or more points.
117  """
118  # You can't set widget properties from outside main thread, so create a signal to updat with new sensor readings
119  update_gui = pyqtSignal()
120 
121  def __init__(self, joint_name, calibration_changed_callback, calibration_points, tweak_callback):
122  super(SrGuiCybergloveJointTweaker, self).__init__()
123 
124  self._tweak_callback = tweak_callback
125  self._joint_name = joint_name
126 
127  self._calibration_points = calibration_points
128  self._calibration_points.sort(key=lambda p: p[1])
129 
130  self.setLayout(QHBoxLayout())
131  self.layout().addWidget(self._get_labels(joint_name))
132 
133  self.layout().addWidget(self._get_points_and_display(self._calibration_points))
134 
135  self.layout().addWidget(self._get_picture(joint_name))
136  self.layout().addStretch()
137 
138  self.update_gui.connect(self._update_gui)
139 
140  # Updating Widgets too fast causes the gui to crash. Update everything at 20hz,
141  # rather than each time a new value is ready.
142  self._timer = rospy.Timer(rospy.Duration(0.05), lambda x: self.update_gui.emit())
143 
144  def _get_picture(self, joint_name):
145  picture = QLabel()
146  picture.setPixmap(QPixmap(rootPath + '/images/%s.jpg' % joint_name))
147  return picture
148 
149  def _get_labels(self, joint_name):
150  labels = QtWidgets.QWidget()
151  labels.setLayout(QVBoxLayout())
152  labels.layout().addWidget(QLabel(joint_name))
153 
154  self._raw_value_label = QLabel()
155  self._calibrated_value_label = QLabel()
156 
157  labels.layout().addWidget(self._calibrated_value_label)
158  labels.layout().addWidget(self._raw_value_label)
159  return labels
160 
161  def _get_points_and_display(self, calibration_points):
162 
163  points = QtWidgets.QWidget()
164  points.setLayout(QHBoxLayout())
165 
166  points_and_display = QtWidgets.QWidget()
167  points_and_display.setLayout(QVBoxLayout())
168  points_and_display.layout().setSpacing(30)
169 
170  for n, point in enumerate(calibration_points):
171  points.layout().addWidget(SrGuiCyberglovePointTweaker(n, point[0], point[1], self._point_change_callback))
172  if n < len(calibration_points) - 1:
173  points.layout().addWidget(QVLine())
174 
175  self._progress_bar = QProgressBar()
176  self._progress_bar.setTextVisible(False)
177  self._progress_bar.setRange(0, 1000)
178 
179  points_and_display.layout().addWidget(self._progress_bar)
180  points_and_display.layout().addWidget(points)
181 
182  return points_and_display
183 
184  def set_calibrated_value(self, value):
185  self._calibrated_value = value
186 
187  def set_raw_value(self, value):
188  self._raw_value = value
189 
190  def _set_progress_bar(self):
191  """
192  Split the sensor range into (number of cal points - 1) windows. Treat each window as a separate linear
193  interpolation, so progress bar shows which calibration points are controlling the current value.
194  """
195 
196  min_calibration = self._calibration_points[0][1]
197  max_calibration = self._calibration_points[-1][1]
198 
199  position = max(self._calibrated_value, min_calibration)
200  position = min(position, max_calibration)
201 
202  window_index = 0
203  number_of_windows = (len(self._calibration_points) - 1)
204 
205  range_per_window = (self._progress_bar.maximum() - self._progress_bar.minimum()) / number_of_windows
206 
207  lower_bound = min_calibration
208  upper_bound = max_calibration
209 
210  # Find which window the sensor value is in. Set upper and lower bounds to the edges the window.
211  for n in range(len(self._calibration_points) - 1):
212  if position > self._calibration_points[n + 1][1]:
213  lower_bound = self._calibration_points[n + 1][1]
214  window_index += 1
215  else:
216  upper_bound = self._calibration_points[n + 1][1]
217  break
218 
219  position_in_window = (position - lower_bound)/(upper_bound - lower_bound)
220  value = (window_index + position_in_window) * range_per_window
221 
222  self._progress_bar.setValue(value)
223 
224  @QtCore.pyqtSlot()
225  def _update_gui(self):
226  try:
227  self._calibrated_value_label.setText("%10.4f (Calibrated)" % self._calibrated_value)
228  self._raw_value_label.setText("%10.4f (Raw)" % self._raw_value)
229  self._set_progress_bar()
230  except RuntimeError as e:
231  # When the gui is closed, different threads die at different times. Sometimes an attempt is
232  # made to write to set the values of a deleted label. This kills the unwanted error messages.
233  if "has been deleted" not in str(e):
234  rospy.logerr("Runtime error while updating gui: %s" % str(e))
235 
236  def _point_change_callback(self, index, value):
237  self._calibration_points[index][0] = value
239 
240 
242  """
243  The plugin used to tweak glove calibrations.
244  """
245  name = "Cyberglove Calibration Tweaker"
246 
247  def _make_listeners(self):
248  self._raw_listner = rospy.Subscriber("/rh_cyberglove/raw/joint_states",
249  JointState, self._raw_callback, queue_size=1)
250  self._calibrated_listner = rospy.Subscriber("/rh_cyberglove/calibrated/joint_states",
251  JointState, self._calibrated_callback, queue_size=1)
252 
253  def _get_calibration_from_parameter(self, calibration=None):
254  if calibration is None:
255  calibration = rospy.get_param(self._calibration_param)
256  self._original_calibration = deepcopy(calibration)
257  self._calibration = {}
258  for entry in calibration:
259  name = entry[0]
260  self._calibration[name] = entry[1]
261 
262  def _raw_callback(self, msg):
263  for n, joint in enumerate(msg.name):
264  if joint in self._joint_tweakers:
265  value = msg.position[n]
266  self._joint_tweakers[joint].set_raw_value(value)
267 
268  def _calibrated_callback(self, msg):
269  for n, joint in enumerate(msg.name):
270  if joint in self._joint_tweakers:
271  value = msg.position[n]
272  self._joint_tweakers[joint].set_calibrated_value(degrees(value))
273 
274  def _tweak_callback(self, joint, calibration):
275  self._calibration[joint] = calibration
277 
278  def _output_calibration_to_parameter(self, calibration=None):
279  if calibration is None:
280  calibration = [[name, self._calibration[name]] for name in self._joint_names]
281  rospy.set_param(self._calibration_param, calibration)
283 
284  def _make_widget(self, context):
285  self._widget = QtWidgets.QWidget()
286 
287  if context is not None:
288  context.add_widget(self._widget)
289 
290  self._top_layout = QVBoxLayout()
291  self._joints_layout = QVBoxLayout()
292  self._widget.setLayout(self._top_layout)
293 
294  top = QtWidgets.QWidget()
295  top.setLayout(self._joints_layout)
296 
297  scroll = QScrollArea()
298  scroll.setWidget(top)
299  scroll.setWidgetResizable(True)
300 
301  self._top_layout.addWidget(scroll)
302 
303  self._top_layout.addWidget(QtWidgets.QWidget())
304 
306 
308 
309  self._make_buttons()
310 
312  self._joint_tweakers = {}
313  self._joint_lines = {}
314 
315  for joint in self._joint_names:
316  joint_tweaker = SrGuiCybergloveJointTweaker(joint, None, self._calibration[joint], self._tweak_callback)
317  joint_tweaker.set_raw_value(0)
318  joint_tweaker.set_calibrated_value(0)
319  self._joint_tweakers[joint] = joint_tweaker
320  self._joints_layout.addWidget(joint_tweaker)
321  self._joint_lines[joint] = QHLine()
322  self._joints_layout.addWidget(self._joint_lines[joint])
323 
324  def _make_calibrer(self):
325  # read nb_sensors from rosparam or fallback to 22
326  if rospy.has_param('rh_cyberglove/cyberglove_joint_number'):
327  self.nb_sensors = rospy.get_param('rh_cyberglove/cyberglove_joint_number')
328  else:
329  self.nb_sensors = 22
330  self._calibrer = CybergloveCalibrer(description_function=None, nb_sensors=self.nb_sensors)
331  self._joint_names = self._calibrer.cyberglove.joints.keys()
332  self._joint_names.sort()
333 
334  def __init__(self, context):
335  super(SrGuiCybergloveTweaker, self).__init__(context)
336  self.setObjectName('SrGuiCybergloveTweaker')
337  self.icon_dir = os.path.join(
338  rospkg.RosPack().get_path('sr_visualization_icons'), '/icons')
339 
340  self._calibration_param = "/rh_cyberglove/cyberglove_calibration"
341 
342  self._make_calibrer()
343 
344  self._make_widget(context)
345 
346  self._make_listeners()
347 
348  self._widget.setWindowTitle('Cyberglove Calibration Tweaker')
349 
350  self._driver_reload_callback = rospy.ServiceProxy("/rh_cyberglove/reload_calibration", Empty)
351 
352  def _make_buttons(self):
353  btn_frame = QtWidgets.QFrame()
354  btn_layout = QtWidgets.QHBoxLayout()
355  btn_layout.setSpacing(25)
356 
357  btn_save = QtWidgets.QPushButton()
358  btn_save.setText("&Save")
359  btn_save.setToolTip("Save the current calibration")
360  btn_save.setIcon(QtGui.QIcon(rootPath + '/images/icons/save.png'))
361  btn_save.clicked.connect(self._save_calibration)
362  btn_layout.addWidget(btn_save)
363 
364  btn_load = QtWidgets.QPushButton()
365  btn_load.setText("&Load")
366  btn_load.setToolTip("Load a Glove calibration")
367  btn_load.setIcon(QtGui.QIcon(rootPath + '/images/icons/load.png'))
368  btn_layout.addWidget(btn_load)
369  btn_load.clicked.connect(self._load_calibration)
370  btn_frame.setLayout(btn_layout)
371 
372  btn_reset = QtWidgets.QPushButton()
373  btn_reset.setText("&Reset")
374  btn_reset.setToolTip("Reset to original/last loaded calibration.")
375  btn_reset.setIcon(QtGui.QIcon(rootPath + '/images/icons/load.png'))
376  btn_layout.addWidget(btn_reset)
377  btn_reset.clicked.connect(self._reset_calibration)
378  btn_frame.setLayout(btn_layout)
379 
380  self._top_layout.addWidget(btn_frame)
381 
382  def _delete_widget(self, layout, widget):
383  layout.removeWidget(widget)
384  sip.delete(widget)
385  widget = None
386 
390  self._reset_tweakers()
391 
392  def _reset_tweakers(self):
393  for joint_name in self._joint_tweakers:
394  self._delete_widget(self._joints_layout, self._joint_tweakers[joint_name])
395  self._delete_widget(self._joints_layout, self._joint_lines[joint_name])
397 
398  def _save_calibration(self):
399  (file_name, dummy) = QtWidgets.QFileDialog.getSaveFileName(
400  self._widget, 'Save Calibration', '')
401  if file_name == "":
402  return
403 
404  text = ["{'cyberglove_calibration': ["]
405 
406  for name in self._joint_names:
407  calibration_string = str(self._calibration[name])
408  text.append("['%s', %s]," % (name, calibration_string))
409  text.append("]}")
410 
411  try:
412  with open(file_name, "w") as output_file:
413  for line in text:
414  output_file.write(line + "\n")
415  except Exception as e:
416  rospy.logerr(str(e))
417  QtWidgets.QMessageBox.information(
418  self._widget, "Calibration saving failed!", "Saving loading failed: %s" % str(e),
419  QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)
420 
421  def _load_calibration(self):
422  (file_name, dummy) = QtWidgets.QFileDialog.getOpenFileName(
423  self._widget, 'Open Calibration', '')
424  if "" == file_name:
425  return
426 
427  if self._calibrer.load_calib(str(file_name)) == 0:
428  QtWidgets.QMessageBox.information(
429  self._widget, "Calibration successfully loaded", "Calibration successfully loaded.",
430  QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)
431  # cyberglove_calibrer already had a function for loading from a file which writes to the parameter.
432  # as we have to set the parameter anyway, we just let calibrer do it and read it back from the param server
433  # it's a little bit dirty, but sue me, it works ;)
434 
436  self._reset_tweakers()
437 
438  else:
439  QtWidgets.QMessageBox.information(
440  self._widget, "Calibration loading failed", "Calibration loading failed",
441  QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)
442 
443 if __name__ == '__main__':
444  import sys
445  rospy.init_node("test_gui")
446  app = QApplication(sys.argv)
447  window = SrGuiCybergloveTweaker(None)
448  window._widget.show()
449  sys.exit(app.exec_())
def __init__(self, joint_name, calibration_changed_callback, calibration_points, tweak_callback)


sr_gui_cyberglove_calibrator
Author(s): Ugo Cupcic
autogenerated on Wed Oct 14 2020 03:22:50