plot_multi_trigger.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 
3 from __future__ import with_statement
4 
5 PKG='pr2_camera_synchronizer'
6 import roslib; roslib.load_manifest(PKG)
7 import rospy
8 from ethercat_trigger_controllers.msg import MultiWaveform
9 import dynamic_reconfigure.client
10 import threading
11 import sys
12 import copy
13 import time
14 import subprocess
15 import atexit
16 
17 # TODO:
18 # - Check that exposure duration is correct for projectorless modes.
19 # - Check modes that aren't synchronized with the projector.
20 
21 slight_shift = 1e-6 #ms
22 
24  def sorted_pts(self):
25  #print >> sys.stderr, self.name
26  #print >> sys.stderr, self.pts
27  pts = [ (x % self.period, y) for (x, y) in self.pts ]
28  firstpt = pts.index(min(pts, key = lambda xy:xy[0]))
29  pts = pts[firstpt:] + pts[:firstpt]
30  #print >> sys.stderr, pts
31  #print >> sys.stderr
32  return pts
33 
34  def intervals(self):
35  if not self.pts:
36  return []
37  if self.pts[-1][1]:
38  raise Exception("Last point in sequence has non-zero y coord in %s."%self.name)
39  intervals = []
40  idx = 0
41  while idx < len(self.pts):
42  if self.pts[idx][1]:
43  startidx = idx
44  max_value = 0
45  while self.pts[idx][1]:
46  max_value = max(max_value, self.pts[idx][1])
47  idx += 1
48  intervals.append((self.pts[startidx][0], self.pts[idx][0], max_value))
49  idx += 1
50  return intervals
51 
52 def replicate_interval(intervals, period, reps):
53  out = list(intervals)
54  for i in range(1, reps):
55  offset = i * period
56  out += [(s+offset,e+offset,v) for (s,e,v) in intervals]
57  return out
58 
59 def interval_lengths(intervals):
60  return [e-s for s,e,v in intervals]
61 
62 def interval_intersection(a, b, period):
63  a0, a1, _ = a
64  b0, b1, _ = b
65 
66  a0 += period
67  a1 += period
68  b0 += period
69  b1 += period
70 
71  # Assume that all these values are now positive.
72  assert(a0 > 0)
73  assert(a1 > 0)
74  assert(b0 > 0)
75  assert(b1 > 0)
76 
77  # Get all the intervals to start in the first period.
78  a0w = a0 % period
79  a1w = a1 + a0w - a0
80  b0w = b0 % period
81  b1w = b1 + b0w - b0
82 
83  # Get the second interval to start before the first, but not more than
84  # one period before.
85  if b0w > a0w:
86  b0w -= period
87  b1w -= period
88 
89  # How much intersection?
90  total = max(0, min(b1w, a1w) - max(b0w, a0w))
91 
92  # How much intersection when the second interval gets shifted forward
93  # one period?
94 
95  b0w += period
96  b1w += period
97  total += max(0, min(b1w, a1w) - max(b0w, a0w) )
98 
99  #print >> sys.stderr, "Interval intersection", a, b, period, total
100  return total
101 
102 
103 
104 
105 
107  def __init__(self, parent, trigger_name):
108  self.parent = parent
109  parent.add(self)
110  self.name = trigger_name
111  self.ready = False
112  rospy.Subscriber(trigger_name + "/waveform", MultiWaveform, self.callback)
113  self.last_msg = None
114 
115  def compute(self):
116  if not self.ready:
117  return False
118  return True
119 
120  def callback(self, msg):
121  #print >> sys.stderr, "Trigger callback", self.name
122  if self.last_msg == msg:
123  return
124  self.last_msg = msg
125  self.period = msg.period
126  self.ready = True
127  offset = msg.zero_offset
128  self.pts = [ ((trans.time + offset) % self.period, trans.value % 2) for trans in msg.transitions ]
129  self.parent.update()
130 
131 
132 
133 
134 
135 
137  def __init__(self, parent, node_name, trigger):
138  self.name = node_name
139  self.ready = False
140  self.parent = parent
141  parent.add_camera(self)
142  self.trigger = trigger
143  self.trigger_delay = 0.00008
144  # Approximate, but good enough given that everything else is on the 1kHz ethercat clock.
145  self.client = dynamic_reconfigure.client.Client(node_name,
146  config_callback = self.callback)
147 
148  def check(self, projector):
149  try:
150  projector_intervals = projector.intervals()
151  camera_intervals = self.intervals()
152 
153  #print >> sys.stderr
154  #print >> sys.stderr, self.name
155  #print >> sys.stderr, projector_intervals
156  #print >> sys.stderr, camera_intervals
157 
158  reps = self.period / projector.period
159  if reps % 1:
160  raise Exception("Camera %s period is %f, projector period is %f, ratio %f"%
161  (self.name,self.period,projector.period,reps))
162  reps = int(reps)
163 
164  projector_intervals = replicate_interval(projector_intervals, projector.period, reps)
165  projector_pulse_len = max(interval_lengths(projector_intervals)+[0])
166 
167  for s, e, v in camera_intervals:
168  info = "camera %s exposing from %f to %f"%(self.name,s,e)
169  if v == 0.75:
170  alt = True
171  interval = s+0.0004, e-0.0004, v # Conservative check that we don't lose texture light.
172  elif v == 1:
173  alt = False
174  interval = s-0.0005, e+0.0005, v # Conservative check that we don't hit texture light.
175  else:
176  raise Exception("Can't determine register set, %s.", info)
177 
178  red_light_times = [ interval_intersection(interval, pi, self.period) for pi in projector_intervals ]
179  red_light_times.sort()
180  red_light_time = sum(red_light_times)
181  if red_light_time and red_light_time != red_light_times[-1]:
182  #print >> sys.stderr, red_light_times
183  #print >> sys.stderr, projector_intervals
184  raise Exception("Intersection with multiple pulses, %s."%info)
185 
186  if red_light_time and abs(red_light_time - projector_pulse_len) > slight_shift:
187  #print >> sys.stderr, projector_intervals
188  #print >> sys.stderr, projector.pts
189  raise Exception("Partial intersection with pulse (alt=%s), %s, %f s of %f, delta %e."%(alt, info, red_light_time, projector_pulse_len, red_light_time-projector_pulse_len))
190  if alt and not red_light_time:
191  raise Exception("Alternate imager but no projector, %s."%info)
192  if not alt and red_light_time:
193  raise Exception("Primary imager and has projector, %s."%info)
194  with_proj_exp = projector_pulse_len + 0.0015 + 2 * slight_shift
195  if alt and e - s > with_proj_exp:
196  raise Exception("Too long exposure for textured image %f instead of %f, %s."%(e-s,with_proj_exp,info))
197  except Exception as e:
198  #import traceback
199  #traceback.print_exc()
200  self.parent.set_error(repr(e))
201 
202  def compute(self):
203  if not self.ready or not self.trigger.ready:
204  #print >> sys.stderr, "Not ready", self.name
205  return False
206  if not self.config['ext_trig']:
207  #print >> sys.stderr, "Not ext trig", self.name
208  return False
209  self.period = self.trigger.period
210  self.pts = []
211  rising = self.config['rising_edge_trig']
212  before = 0 if rising else 1
213  after = 1 - before
214  pretrigpts = []
215  imager_period = 1.0 / self.config['imager_rate']
216 
217  #print >> sys.stderr, "\n", self.name
218 
219  # Figure out the trigger times and the corresponding register set.
220  tpts = self.trigger.sorted_pts()
221  for i in range(len(tpts)):
222  if tpts[i-1][1] == before and tpts[i][1] == after:
223  pretrigpts.append(tpts[i][0] + self.trigger_delay)
224  #print >> sys.stderr, "preselected", tpts[i]
225  trigpts = []
226 
227  # Run through the triggers once to find the last trigger time in
228  # the sequence.
229  last_trig = -imager_period
230  trig_count = 0
231  last_count = 0
232  for i in range(len(pretrigpts)):
233  trig_count += 1
234  if pretrigpts[i] - last_trig > imager_period:
235  last_trig = pretrigpts[i]
236  last_count = trig_count
237 
238  # Run through the triggers again keeping
239  first_trig = last_trig
240  last_trig = first_trig - self.period
241  for i in range(len(pretrigpts)):
242  trig_count += 1
243  if pretrigpts[i] - last_trig > imager_period:
244  last_trig = pretrigpts[i]
245  reg_set = self.config['register_set']
246  if reg_set == 2:
247  reg_set = 1 if (last_count - trig_count) % 2 else 0
248  trigpts.append((pretrigpts[i], reg_set))
249  last_count = trig_count
250  else:
251  #print >> sys.stderr, "dropped", pretrigpts[i]
252  pass
253  if last_trig != first_trig:
254  self.parent.set_error("Non-consistent triggering times for %s."%self.name)
255 
256  # Figure out exposure times for each register set.
257  cfg_suffix = [ "", "_alternate" ]
258  exposure_min = []
259  exposure_max = []
260  max_exp = self.config['max_exposure']
261  for i in range(2):
262  auto_exp = self.config['auto_exposure'+cfg_suffix[i]]
263  set_exp = self.config['exposure'+cfg_suffix[i]]
264  cur_min = 0
265  cur_max = max_exp
266  if not auto_exp:
267  cur_min = min(max_exp, set_exp)
268  cur_max = cur_min
269  exposure_min.append(cur_min)
270  exposure_max.append(cur_max)
271 
272  # Generate the exposure points.
273  for (t, rs) in trigpts:
274  alternate_offset = - rs * 0.25
275  exp_end = t + imager_period
276  exp_start_early = exp_end - exposure_max[rs]
277  exp_start_late = exp_end - exposure_min[rs]
278  if exp_start_early != exp_start_late:
279  self.pts.append((exp_start_early - slight_shift, 0.5 + alternate_offset))
280  self.pts.append((exp_start_late, 1 + alternate_offset))
281  self.pts.append((exp_end + slight_shift, 0))
282  return True
283 
284  def callback(self, config):
285  #print >> sys.stderr, "Camera callback", self.name
286  if self.ready and self.config == config:
287  return
288  self.config = copy.deepcopy(config)
289  self.ready = True
290  self.parent.update()
291 
292 
293 
294 
295 
296 
298  def __init__(self, plot, silent = False):
299  self.do_plot = plot
300  self.triggers = []
301  self.mutex = threading.Lock()
302  self.error = None
303  self.updated = False
304  self.cameras = []
305  self.projector = None
306  self.silent = silent
307  if self.do_plot:
308  try:
309  self.gnuplot = subprocess.Popen('gnuplot', stdin = subprocess.PIPE)
310  except:
311  print("Gnuplot must be installed to allow plotting of waveforms.")
312  sys.exit(1)
313  atexit.register(self.gnuplot.kill)
314 
315  def add(self, trigger):
316  self.triggers.append(trigger)
317 
318  def add_camera(self, cam):
319  self.cameras.append(cam)
320  self.add(cam)
321 
322  def set_projector(self, projector):
323  self.projector = projector
324 
325  def clear(self):
326  with self.mutex:
327  for trig in self.triggers:
328  trig.ready = False
329  self.updated = False
330 
331  def update(self):
332  with self.mutex:
333  self.error = None
334  n = len(self.triggers)
335  for i in range(n):
336  if not self.triggers[i].compute():
337  if not self.silent:
338  self.set_error('No data for %s. (This is normal at startup.)'%self.triggers[i].name)
339  return
340 
341  if self.do_plot:
342  if self.error:
343  self.empty_plot()
344  else:
345  self.plot()
346  if self.projector:
347  for c in self.cameras:
348  c.check(self.projector)
349  if not self.silent:
350  if not self.error:
351  rospy.loginfo("All checks passed.")
352  else:
353  rospy.logerr("Some checks failed.")
354  self.updated = True
355 
356  def set_error(self, msg):
357  if not self.silent:
358  rospy.logerr(msg)
359  self.error = msg
360 
361  def plot(self):
362  n = len(self.triggers)
363  period = max(t.period for t in self.triggers)
364  for i in range(n):
365  if (period / self.triggers[i].period) % 1:
366  self.set_error('Period for %s is %f, expected divisor of %f'%(self.triggers[i].name,
367  self.triggers[i].period, period))
368  return
369  style = 'with linespoints title "%s"'
370  print('plot "-"', style%self.triggers[0].name, end=' ', file=self.gnuplot.stdin)
371  for i in range(1, n):
372  print(', "-"', style%self.triggers[i].name, end=' ', file=self.gnuplot.stdin)
373  print(file=self.gnuplot.stdin)
374  for i in range(n):
375  t = self.triggers[i]
376  reps = int(period / t.period)
377  pts = t.sorted_pts()
378  if len(pts) == 0:
379  pts = [(0, 0)]
380  def plot_pt(x, y):
381  print(x, (n - i - 1) * 1.1 + y%2, file=self.gnuplot.stdin)
382  plot_pt(0, pts[-1][1])
383  for k in range(reps):
384  xoffs = t.period * k
385  for j in range(len(pts)):
386  plot_pt(pts[j][0] + xoffs, pts[j-1][1])
387  plot_pt(pts[j][0] + xoffs, pts[j][1])
388  plot_pt(period, pts[-1][1])
389  print("e", file=self.gnuplot.stdin)
390  print(file=self.gnuplot.stdin)
391  sys.stdout.flush()
392 
393  def empty_plot(self):
394  print('plot x, -x')
395 
396 import unittest
397 class Test(unittest.TestCase):
398  def setUp(self):
399  global tp, reconfig_client
400  tp.silent = True
401  self.reconfig_client = reconfig_client
402  tp.clear()
403  self.reconfig_client.update_configuration({'projector_mode': 1})
404 
405  def wait_for_ready(self):
406  global tp
407  time.sleep(2)
408  starttime = time.time()
409  while not rospy.is_shutdown():
410  if time.time() > starttime + 5:
411  self.fail("Took too long to get responses.")
412  if tp.updated:
413  break
414  time.sleep(0.1)
415 
416  def evaluate(self):
417  global tp
418  self.wait_for_ready()
419  tp.silent = False
420  tp.update()
421  self.assertFalse(tp.error, tp.error)
422 
423  def test_with(self):
424  self.reconfig_client.update_configuration({
425  'projector_pulse_shift': 0.0,
426  'wide_stereo_trig_mode': 3,
427  'prosilica_projector_inhibit': False,
428  'camera_reset': False,
429  'forearm_r_rate': 30.0,
430  'projector_rate': 58.823529411764703,
431  'stereo_rate': 29.411764705882351,
432  'projector_pulse_length': 0.002,
433  'projector_mode': 2,
434  'forearm_r_trig_mode': 3,
435  'projector_tweak': 0.0,
436  'forearm_l_trig_mode': 3,
437  'forearm_l_rate': 30.0,
438  'narrow_stereo_trig_mode': 3,
439  })
440  self.evaluate()
441 
442  def test_without(self):
443  self.reconfig_client.update_configuration({
444  'projector_pulse_shift': 0.0,
445  'wide_stereo_trig_mode': 4,
446  'prosilica_projector_inhibit': False,
447  'camera_reset': False,
448  'forearm_r_rate': 30.0,
449  'projector_rate': 58.823529411764703,
450  'stereo_rate': 29.411764705882351,
451  'projector_pulse_length': 0.002,
452  'projector_mode': 3,
453  'forearm_r_trig_mode': 4,
454  'projector_tweak': 0.0,
455  'forearm_l_trig_mode': 4,
456  'forearm_l_rate': 30.0,
457  'narrow_stereo_trig_mode': 4,
458  })
459  self.evaluate()
460 
461  def test_alt(self):
462  self.reconfig_client.update_configuration({
463  'projector_pulse_shift': 0.0,
464  'wide_stereo_trig_mode': 4,
465  'prosilica_projector_inhibit': False,
466  'camera_reset': False,
467  'forearm_r_rate': 30.0,
468  'projector_rate': 58.823529411764703,
469  'stereo_rate': 29.411764705882351,
470  'projector_pulse_length': 0.002,
471  'projector_mode': 3,
472  'forearm_r_trig_mode': 4,
473  'projector_tweak': 0.0,
474  'forearm_l_trig_mode': 4,
475  'forearm_l_rate': 30.0,
476  'narrow_stereo_trig_mode': 5,
477  })
478  self.evaluate()
479 
480  def test_slow(self):
481  self.reconfig_client.update_configuration({
482  'projector_pulse_shift': 0.0,
483  'wide_stereo_trig_mode': 4,
484  'prosilica_projector_inhibit': False,
485  'camera_reset': False,
486  'forearm_r_rate': 5.0,
487  'projector_rate': 58.823529411764703,
488  'stereo_rate': 5.0,
489  'projector_pulse_length': 0.002,
490  'projector_mode': 3,
491  'forearm_r_trig_mode': 4,
492  'projector_tweak': 0.0,
493  'forearm_l_trig_mode': 3,
494  'forearm_l_rate': 5.0,
495  'narrow_stereo_trig_mode': 5,
496  })
497  self.evaluate()
498 
499 def main():
500  global tp, reconfig_client
501  regression_test = rospy.get_param('~regression_test', False)
502  tp = TriggerChecker(plot = not regression_test, silent = regression_test)
503  head_trig = Trigger(tp, '/head_camera_trigger')
504  l_forearm_trig = Trigger(tp, 'l_forearm_cam_trigger')
505  r_forearm_trig = Trigger(tp, 'r_forearm_cam_trigger')
506  Camera(tp, '/narrow_stereo_both', head_trig)
507  tp.set_projector(Trigger(tp, '/projector_trigger'))
508  Camera(tp, '/wide_stereo_both', head_trig)
509  Camera(tp, '/l_forearm_cam', l_forearm_trig)
510  Camera(tp, '/r_forearm_cam', r_forearm_trig)
511  if regression_test:
512  import rostest
513  rospy.loginfo("Running in unit test mode")
514  reconfig_client = dynamic_reconfigure.client.Client('synchronizer')
515  rostest.rosrun(PKG, 'test_bare_bones', Test)
516  else:
517  rospy.loginfo("Running in plotting mode")
518  while not rospy.is_shutdown():
519  time.sleep(0.1)
520 
521 if __name__ == "__main__":
522  rospy.init_node('trigger_plotter', anonymous = True)
523  main()
def __init__(self, parent, node_name, trigger)
def __init__(self, plot, silent=False)
def replicate_interval(intervals, period, reps)
def check(self, projector)
def __init__(self, parent, trigger_name)
def interval_intersection(a, b, period)
def interval_lengths(intervals)


pr2_camera_synchronizer
Author(s): Blaise Gassend
autogenerated on Tue Jun 1 2021 02:50:50