class MyBlockB(Block): Block.input_is_variable() Block.output('only one')
class MPlayer(Generator): ''' Decodes a video stream. ''' Block.alias('mplayer') Block.config( 'file', 'Input video file. This can be in any format that ' '``mplayer`` understands.') Block.config('quiet', 'If true, suppress stderr messages from mplayer.', default=True) Block.config('stats', 'If true, writes some statistics about the ' 'remaining time.', default=True) Block.config('max_duration', 'Maximum length, in seconds, of the output.' 'Useful to get a maximum duration.', default=None) Block.output('video', 'RGB stream as numpy array.') TIMESTAMPS_SUFFIX = ".timestamps" def init(self): self.programs = check_programs_existence(programs=['mencoder']) for p in self.programs: self.info('Using %13s = %s' % (p, self.programs[p])) if not isinstance(self.config.file, str): raise BadConfig('This should be a string.', self, 'file') self.file = expand(self.config.file) if not os.path.exists(self.file): msg = 'File %r does not exist.' % self.file raise BadConfig(msg, self, 'file') timestamps_file = self.file + MPlayer.TIMESTAMPS_SUFFIX if os.path.exists(timestamps_file): self.info('Reading timestamps from %r.' % timestamps_file) self.timestamps = open(timestamps_file) else: self.timestamps = None #self.info('Will use fps for timestamps.') self.mencoder_started = False self.state.timestamp = None self.state.timestamp = self.get_next_timestamp() self.state.next_frame = None self.state.finished = False self.num_frames_read = 0 def open_mencoder(self): self.mencoder_started = True info = pg_video_info(self.file, intolerant=True) self.width = info['width'] self.height = info['height'] self.fps = info['fps'] self.length = info['length'] check('float|int', self.length) check('float|int', self.fps) self.info('length: %r' % self.length) self.info('fps: %r' % self.fps) self.approx_frames = int(math.ceil(self.length * self.fps)) # TODO: reading non-RGB streams not supported self.info('Reading %dx%d @ %.3f fps ' ' (length %ss, approx %d frames), from %s.' % (self.width, self.height, self.fps, self.length, self.approx_frames, friendly_path(self.config.file))) self.shape = (self.height, self.width, 3) self.dtype = 'uint8' pixel_format = "rgb24" self.temp_dir = tempfile.mkdtemp(prefix='procgraph_fifo_dir') self.fifo_name = os.path.join(self.temp_dir, 'mencoder_fifo') os.mkfifo(self.fifo_name) args = [ self.programs['mencoder'], self.file, '-ovc', 'raw', '-rawvideo', 'w=%d:h=%d:format=%s' % (self.width, self.height, pixel_format), '-of', 'rawvideo', '-vf', 'format=rgb24', '-nosound', '-o', self.fifo_name ] self.tmp_stdout = tempfile.TemporaryFile() self.tmp_stderr = tempfile.TemporaryFile() if self.config.quiet: self.process = subprocess.Popen(args, stdout=self.tmp_stdout.fileno(), stderr=self.tmp_stderr.fileno()) else: self.process = subprocess.Popen(args) self.delta = 1.0 / self.fps if not self.timestamps: msg = ('No timestamps found; using delta = %.2f (%.2f fps).' % (self.delta, self.fps)) self.info(msg) self.stream = open(self.fifo_name, 'r') def get_next_timestamp(self): if self.timestamps: # If reading from files l = self.timestamps.readline() if not l: # XXX warn if not even one self.error_once( 'Timestamps were too short; now using incremental.') if self.state.timestamp is not None: return self.state.timestamp + self.delta else: # empty file, not even one self.error_once('Empty timestamp file? Starting at 0.') return 0.0 else: return float(l) else: if self.state.timestamp is None: return 0.0 else: return self.state.timestamp + self.delta def update(self): if not self.mencoder_started: self.open_mencoder() self.read_next_frame() self.set_output(0, value=self.state.next_frame, timestamp=self.state.timestamp) self.state.timestamp = self.get_next_timestamp() self.state.next_frame = None self.read_next_frame() if self.config.stats: self.num_frames_read += 1 if self.num_frames_read % 500 == 0: self.print_stats() def print_stats(self): if self.approx_frames != 0: percentage = 100.0 * self.num_frames_read / self.approx_frames else: percentage = 0 # this assumes constant fps seconds = self.num_frames_read * self.delta seconds_total = self.approx_frames * self.delta self.info('%6d/%d frames, %.1f/%.1f sec (%4.1f%%) of %s' % (self.num_frames_read, self.approx_frames, seconds, seconds_total, percentage, friendly_path(self.file))) def read_next_frame(self): if self.state.finished: return if self.state.next_frame is not None: return # TODO: add display of stderr after timeout dtype = numpy.dtype(('uint8', self.shape)) rgbs = numpy.fromfile(self.stream, dtype=dtype, count=1) if len(rgbs) == 0: self.state.next_frame = None self.state.finished = True else: self.state.next_frame = rgbs[0, :].squeeze() def too_long_for_us(self): """ Returns true if max_duration is set and we passed it. """ if self.config.max_duration is None: return False # We haven't started yet, so we don't have "delta" if not self.mencoder_started: return False passed = self.num_frames_read * self.delta if passed > self.config.max_duration: self.info_once( 'Finishing because passed %d frames = %f seconds > duration %f' % (self.num_frames_read, passed, self.config.max_duration)) return True else: return False def next_data_status(self): # The stream has finished if self.state.finished: return (False, None) # We passed the limit elif self.too_long_for_us(): return (False, None) else: return (True, self.state.timestamp) def finish(self): # TODO: make sure process is closed? if os.path.exists(self.fifo_name): os.unlink(self.fifo_name) if os.path.exists(self.temp_dir): os.rmdir(self.temp_dir)
class MyBlock(Block): Block.output('x') Block.output('x')
class MyBlock(Block): Block.output_is_variable() Block.output('x')
class VehiclesCairoDisplay(Block): ''' Produces a top-down plot of a circular arena. ''' Block.alias('vehicles_cairo_display') Block.config('format', 'pdf|png', default='pdf') Block.config('file', 'Output file (pdf)', default=None) Block.output('rgb', 'RGB data (png)') Block.config('transparent', 'Outputs RGB with transparent bg', default=False) Block.config('width', 'Image width in points.', default=600) Block.config('height', 'Image height in points.', default=600) Block.config('sidebar_width', default=200) # Sidebar options Block.config('display_sidebar', default=True) Block.config('trace', 'Trace the path', default=False) Block.config('plotting_params', 'Configuration to pass to vehicles_cairo_display_all()', default={}) Block.config('sidebar_params', 'Configuration to pass to create_sidebar()', default={}) Block.config('swf', 'Converts PDF to SWF using pdf2swf', default=True) Block.input('boot_obs', '') def get_shape(self): w = self.config.width if self.config.display_sidebar: w += self.config.sidebar_width h = self.config.height return (w, h) def init(self): self.format = self.config.format (w, h) = self.get_shape() self.total_width = w self.total_height = h self.frame = 0 if self.format == 'pdf': self.init_pdf() elif self.format == 'png': self.init_png() else: raise BadConfig('Invalid format %r.' % self.format, self, 'format') self.count = 0 self.fps = None self.t0 = None self.tmp_cr = None def init_pdf(self): self.filename = self.config.file self.tmp_filename = self.filename + '.active' make_sure_dir_exists(self.filename) self.info("Creating file %r." % self.filename) import cairo self.surf = cairo.PDFSurface(self.tmp_filename, # @UndefinedVariable self.total_width, self.total_height) def init_png(self): import cairo w, h = self.total_width, self.total_height # note (w,h) here and (h,w,h*4) below; I'm not sure but it works self.argb_data = np.empty((h, w, 4), dtype=np.uint8) self.argb_data.fill(255) self.surf = cairo.ImageSurface.create_for_data(# @UndefinedVariable self.argb_data, cairo.FORMAT_ARGB32, # @UndefinedVariable w, h, w * 4) def update(self): # Estimate fps if self.count == 0: self.t0 = self.get_input_timestamp(0) if self.count >= 1: delta = self.get_input_timestamp(0) - self.t0 self.fps = 1.0 * self.count / delta self.count += 1 if self.format == 'pdf': self.update_pdf() elif self.format == 'png': self.update_png() else: assert False def update_png(self): import cairo cr = cairo.Context(self.surf) # @UndefinedVariable self.draw_everything(cr) self.surf.flush() if not self.config.transparent: rgb = self.argb_data[:, :, :3].copy() # fix red/blue inversion rgb[:, :, 0] = self.argb_data[:, :, 2] rgb[:, :, 2] = self.argb_data[:, :, 0] assert rgb.shape[2] == 3 else: rgb = self.argb_data.copy() # fix red/blue inversion rgb[:, :, 0] = self.argb_data[:, :, 2] rgb[:, :, 2] = self.argb_data[:, :, 0] assert rgb.shape[2] == 4 self.output.rgb = rgb def update_pdf(self): import cairo # If I don't recreate it, it will crash cr = cairo.Context(self.surf) # @UndefinedVariable if not self.config.transparent: # Set white background bg_color = [1, 1, 1] cr.rectangle(0, 0, self.total_width, self.total_height) cr.set_source_rgb(bg_color[0], bg_color[1], bg_color[2]) cr.fill() else: # Green screen :-) cr.rectangle(0, 0, self.total_width, self.total_height) cr.set_source_rgba(0, 1, 0, 0) cr.fill() self.draw_everything(cr) self.surf.flush() self.surf.show_page() # Free memory self.cr? def draw_everything(self, cr): boot_obs = self.input.boot_obs if 'id_episode' in boot_obs: id_episode = boot_obs['id_episode'].item() else: id_episode = '' id_vehicle = boot_obs['id_robot'].item() if 'extra' in boot_obs: extra = boot_obs['extra'].item() else: extra = {} def extra_draw_world(cr): if 'servonav' in extra: plot_servonave(cr, extra['servonav']) if 'servoing_poses' in extra: plot_servoing_poses(cr, extra['servoing_poses']) plotting_params = self.config.plotting_params plotting_params['extra_draw_world'] = extra_draw_world sidebar_params = self.config.sidebar_params # todo: check sim_state = extra['robot_state'] observations_values = boot_obs['observations'] commands = boot_obs['commands'] commands_source = boot_obs['commands_source'].item() timestamp = boot_obs['time_from_episode_start'].item() with cairo_save(cr): if self.config.display_sidebar: padding = 0.03 * self.config.width map_width = self.config.width - 2 * padding map_height = self.config.height - 2 * padding cr.translate(padding, padding) else: map_width = self.config.width map_height = self.config.height with cairo_save(cr): cr.rectangle(0, 0, map_width, map_height) cr.clip() # TODO: implement trace vehicles_cairo_display_all(cr, map_width, map_height, sim_state, **plotting_params) if self.config.display_sidebar: cr.set_line_width(1) cr.set_source_rgb(0, 0, 0) cr.rectangle(0, 0, map_width, map_height) cr.stroke() if self.config.display_sidebar: with cairo_transform(cr, t=[self.config.width, 0]): create_sidebar(cr, width=self.config.sidebar_width, height=self.config.height, sim_state=sim_state, id_vehicle=id_vehicle, id_episode=id_episode, timestamp=timestamp, observations_values=observations_values, commands_values=commands, commands_source=commands_source, **sidebar_params) def finish(self): if self.format == 'pdf': self.finish_pdf() def finish_pdf(self): self.surf.finish() if os.path.exists(self.filename): os.unlink(self.filename) if os.path.exists(self.tmp_filename): os.rename(self.tmp_filename, self.filename) if self.config.swf: if self.fps is None: self.error('Only one frame seen?') else: basename, _ = os.path.splitext(self.filename) swf = '%s.swf' % basename try: command = ['pdf2swf', # "-b", # --defaultviewer # "-l", # --defaultloader '-G', # flatten '-s', 'framerate=%d' % self.fps, self.filename, '-o', swf] self.info(' '.join(command)) subprocess.check_call(command) except Exception as e: self.error('Could not convert to swf: %s' % e) if os.path.exists(swf): os.unlink(swf) self.info("Completed %r." % self.filename)
class BlockSimpleExample(Block): ''' This is a documented example of the simplest block possible. This docstring will be included in the generated documentation. ''' # Define a name for the block using `Block.alias`. # If not given, the class name ('BlockExample') will be used. Block.alias('block_example') # Define the inputs for the blocks using `Block.input`. # It takes two parameters: name of the signal, and a docstring. # ProcGraph will generate wonderful documentation for you, and lets # you write documentation for what you are doing at almost every step: # take every chance to document things. Block.input('baz', 'Measured baz in the particle accelerator.') # This block has one output as well. Block.output('baz_compensated', 'Compensated baz value according to calibration.') # Now define the configuration for the block. # Pass the optional 'default' argument to define a default for the # parameter. # Without the default, the user *must* pass the configuration parameter. Block.config('bias', 'Bias for the accelerator.', default=0) # Now let's write the logic for the block. # Very important! Do not use __init__ for initialization. Use init(). # ProcGraph does a lot behind the scenes to let you have a streamlined # experiences, but sometimes there is a price to pay. # # def __init__(self): # ... # # Initialize the block in the init() function. def init(self): # For this example, we have nothing to do here. pass # You will write the main logic in the update() function. # It is called whenever the input signals are updated. # In update() you do your computations, and update the value # of the output signals. def update(self): # You can access the value of the configuration parameters # using `self.config.<parameter name>` bias = self.config.bias # You read the input signals using `self.input.<signal name>`: baz = self.input.baz baz_compensated = baz + bias # Finally, you set the output using `self.output.<signal name> = ...`. self.output.baz_compensated = baz_compensated # You're done: ProcGraph will propagate this value to the other blocks # connected... # Note that for stateless computations such as this, creating a Block # class is likely overkill -- use register_simple_block() with an # instantaneous function instead -- see next example. # Finish is called at the end of the execution. def finish(self): # For this example, we have nothing to do here. pass
class FilesFromDir(Generator): ''' This block reads the filenames from a directory according to a given regexp. For now the timestamp starts from 0 and it is fixed. ''' Block.alias('files_from_dir') Block.config('dir', 'Directory containing the image files.') Block.config('regexp', 'Regular expression for images.', # default='(\w+)(\d+)\.(\w+)') default='.+\.(png|jpg)') Block.config('fps', 'Fixed frame per second.', default=20.0) Block.output('filename', 'Image filename') def init(self): dirname = self.config.dir dirname = expand(dirname) if not os.path.exists(dirname): raise BadConfig('Not existent directory %r.' % dirname, self, 'dir') if not os.path.isdir(dirname): raise BadConfig('The file %r is not a directory.' % dirname, self, 'dir') regexp = self.config.regexp # TODO: use proper logging self.info("Reading %r from %r." % (regexp, dirname)) all_files = os.listdir(dirname) selected = [os.path.join(dirname, f) for f in all_files if re.match(regexp, f)] def natural_key(string): """See http://www.codinghorror.com/blog/archives/001018.html""" return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string)] selected.sort(key=lambda x: natural_key(os.path.basename(x))) self.info("Selected %d/%d files." % (len(selected), len(all_files))) fps = float(self.config.fps) if fps <= 0: raise BadConfig(self, "Invalid fps value %s." % fps, 'fps') # tuples (timestamp, filename) frames = [(i / fps, f) for i, f in enumerate(selected)] if not frames: raise Exception('No frames found in dir %r.' % dirname) self.state.frames = frames self.state.next_frame = 0 def next_data_status(self): k = self.state.next_frame if k is None: return (False, None) else: frames = self.state.frames timestamp = frames[k][0] return (True, timestamp) def update(self): frames = self.state.frames k = self.state.next_frame assert k < len(frames) timestamp, filename = frames[k] self.set_output(0, value=filename, timestamp=timestamp) if k + 1 >= len(frames): self.state.next_frame = None else: self.state.next_frame = k + 1
class LaserDotDisplay(Block): ''' Produces a plot of a range-finder scan variation (derivative). It is a variation of :ref:`block:laser_display`; look there for the documentation. ''' Block.alias('laser_dot_display') Block.config('width', 'Width of the resulting image.', default=320) Block.config('height', 'Height of the resulting image.', default=320) Block.config('groups', 'How to group and draw the readings. ' ' (see :ref:`block:laser_display`) ') Block.config('title', 'By default it displays the signal name.' ' Set the empty string to disable.', default=None) Block.config('transparent', 'Gives transparent RGBA rather than RGB.', default=False) Block.config('R0', 'Radius of the readings circle.', default=1) Block.config('amp', 'Amplitude of the readings crown.', default=0.5) Block.input('readings_dot', 'Array of float representing array readings.') Block.output('image', 'A fancy visualization of the laser derivative') def update(self): y = array(self.input.readings_dot) from procgraph_mpl import pylab2rgb, pylab if max(abs(y)) > 1: raise BadInput('I expect an input normalized in the [-1,1] range;' 'min,max: %s,%s ' % (min(y), max(y)), self, 'readings_dot') f = pylab.figure(frameon=False, figsize=(self.config.width / 100.0, self.config.height / 100.0)) R0 = self.config.R0 amp = self.config.amp theta = linspace(0, 2 * math.pi, 300) pylab.plot(R0 * cos(theta), R0 * sin(theta), 'k--') for group in self.config.groups: indices = group['indices'] indices = range(indices[0], indices[-1] + 1) theta_spec = group['theta'] origin = group.get('origin', [0, 0, 0]) color = group.get('color', 'b.') N = len(indices) theta = linspace(theta_spec[0], theta_spec[-1], N) group_y = y[indices] r = R0 + amp * group_y px = cos(theta + origin[2]) * r py = sin(theta + origin[2]) * r pylab.plot(-py, px, color) M = R0 + amp * 1.2 pylab.axis([-M, M, -M, M]) # turn off ticks labels, they don't have meaning pylab.setp(f.axes[0].get_xticklabels(), visible=False) pylab.setp(f.axes[0].get_yticklabels(), visible=False) if self.config.title is not None: if self.config.title != "": pylab.title(self.config.title, fontsize=10) else: # We don't have a title --- t = self.get_input_signals_names()[0] pylab.title(t, fontsize=10) self.output.image = pylab2rgb(transparent=self.config.transparent) pylab.close(f.number)
class Text(Block): ''' This block provides text overlays over an image. This block is very powerful, but the configuration is a bit complicated. You should provide a list of dictionary in the configuration variable ``texts``. Each dictionary in the list describes how and where to write one piece of text. An example of valid configuration is the following: :: text.texts = [{string: "raw image", position: [10,30], halign: left, color: black, bg: white }] The meaning of the fields is as follow: ``string`` Text to display. See the section below about keyword expansion. ``position`` Array of two integers giving the position of the text in the image ``color`` Text color. It can be a keyword color or an hexadecimal string (``white`` or ``#ffffff``). ``bg`` background color ``halign`` Horizontal alignment. Choose between ``left`` (default), ``center``, ``right``. ``valign`` Vertical alignment. Choose between ``top`` (default), ``middle``, ``center``. ``size`` Font size in pixels ``font`` Font family. Must be a ttf file (``Arial.ttf``) **Expansion**: Also we expand macros in the text using ``format()``. The available keywords are: ``frame`` number of frames since the beginning ``time`` seconds since the beginning of the log ``timestamp`` absolute timestamp ''' Block.alias('text') Block.config('texts', 'Text specification (see block description).') Block.input('rgb', 'Input image.') Block.output('rgb', 'Output image with overlaid text.') def init(self): self.state.first_timestamp = None def update(self): # TODO: add check if self.state.first_timestamp is None: self.state.first_timestamp = self.get_input_timestamp(0) self.state.frame = 0 else: self.state.frame += 1 # Add stats macros = {} macros['timestamp'] = self.get_input_timestamp(0) if self.state.first_timestamp == ETERNITY: macros['time'] = -1 else: macros['time'] = \ self.get_input_timestamp(0) - self.state.first_timestamp macros['frame'] = self.state.frame rgb = self.input.rgb im = Image_from_array(rgb) from . import ImageDraw draw = ImageDraw.Draw(im) # {string: "raw image", position: [10,30], halign: left, # color: black, bg: white } if not isinstance(self.config.texts, list): raise BadConfig('Expected list', self, 'texts') for text in self.config.texts: text = text.copy() if not 'string' in text: raise BadConfig('Missing field "string" in text spec %s.' % text.__repr__(), self, 'texts') try: text['string'] = Text.replace(text['string'], macros) except KeyError as e: msg = str(e) + '\nAvailable: %s.' % macros.keys() raise BadConfig(msg, self, 'texts') p = text['position'] try: p[0] = get_ver_pos_value(p[0], height=rgb.shape[0]) p[1] = get_hor_pos_value(p[1], width=rgb.shape[1]) except ValueError as e: raise BadConfig(str(e), text.__repr__(), self, 'texts') process_text(draw, text) out = im.convert("RGB") pixel_data = numpy.asarray(out) self.output.rgb = pixel_data @staticmethod def replace(s, macros): ''' Expand macros in the text. ''' return s.format(**macros)