$search
00001 # Software License Agreement (BSD License) 00002 # 00003 # Copyright (c) 2009, Willow Garage, Inc. 00004 # All rights reserved. 00005 # 00006 # Redistribution and use in source and binary forms, with or without 00007 # modification, are permitted provided that the following conditions 00008 # are met: 00009 # 00010 # * Redistributions of source code must retain the above copyright 00011 # notice, this list of conditions and the following disclaimer. 00012 # * Redistributions in binary form must reproduce the above 00013 # copyright notice, this list of conditions and the following 00014 # disclaimer in the documentation and/or other materials provided 00015 # with the distribution. 00016 # * Neither the name of Willow Garage, Inc. nor the names of its 00017 # contributors may be used to endorse or promote products derived 00018 # from this software without specific prior written permission. 00019 # 00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 00024 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 00031 # POSSIBILITY OF SUCH DAMAGE. 00032 00033 PKG = 'rxbag_plugins' 00034 import roslib; roslib.load_manifest(PKG) 00035 00036 import os 00037 import shutil 00038 import sys 00039 import threading 00040 import time 00041 00042 import numpy 00043 import wx 00044 import wx.lib.masked 00045 import wx.lib.wxcairo 00046 00047 import Image 00048 00049 from rxbag import bag_helper, TopicMessageView 00050 import image_helper 00051 00052 class ImageView(TopicMessageView): 00053 name = 'Image' 00054 00055 def __init__(self, timeline, parent): 00056 TopicMessageView.__init__(self, timeline, parent) 00057 00058 self._image_lock = threading.RLock() 00059 self._image = None 00060 self._image_topic = None 00061 self._image_stamp = None 00062 00063 self._image_surface = None 00064 00065 self._overlay_font_size = 14.0 00066 self._overlay_indent = (4, 4) 00067 self._overlay_color = (0.2, 0.2, 1.0) 00068 00069 self._size_set = False 00070 00071 self._next_frame_num = 1 00072 00073 tb = self.parent.GetToolBar() 00074 icons_dir = roslib.packages.get_pkg_dir(PKG) + '/icons/' 00075 tb.AddSeparator() 00076 00077 self.save_frame_tool = tb.AddLabelTool(wx.ID_ANY, '', wx.Bitmap(icons_dir + 'picture_save.png'), shortHelp='Save frame', longHelp='Save frame using filename spec') 00078 self.file_spec_tool = wx.TextCtrl(tb, wx.ID_ANY, 'frame%04d.png', size=(200, 22)) 00079 tb.AddControl(self.file_spec_tool) 00080 self.save_selected_frames_tool = tb.AddLabelTool(wx.ID_ANY, '', wx.Bitmap(icons_dir + 'pictures_save.png'), shortHelp='Save frames', longHelp='Save frames from selected region') 00081 00082 tb.Bind(wx.EVT_TOOL, lambda e: self.save_frame(), self.save_frame_tool) 00083 tb.Bind(wx.EVT_TOOL, lambda e: self.save_selected_frames(), self.save_selected_frames_tool) 00084 00085 self.parent.Bind(wx.EVT_SIZE, self.on_size) 00086 self.parent.Bind(wx.EVT_PAINT, self.on_paint) 00087 self.parent.Bind(wx.EVT_RIGHT_DOWN, self.on_right_down) 00088 00089 ## MessageView implementation 00090 00091 def message_viewed(self, bag, msg_details): 00092 TopicMessageView.message_viewed(self, bag, msg_details) 00093 00094 topic, msg, t = msg_details[:3] 00095 00096 if not msg: 00097 self.set_image(None, topic, stamp) 00098 else: 00099 self.set_image(msg, topic, msg.header.stamp) 00100 00101 if not self._size_set: 00102 self._size_set = True 00103 wx.CallAfter(self.reset_size) 00104 00105 def message_cleared(self): 00106 TopicMessageView.message_cleared(self) 00107 00108 self.set_image(None, None, None) 00109 00110 ## 00111 00112 def set_image(self, image_msg, image_topic, image_stamp): 00113 with self._image_lock: 00114 self._image_msg = image_msg 00115 if image_msg: 00116 self._image = image_helper.imgmsg_to_pil(image_msg) 00117 else: 00118 self._image = None 00119 self._image_surface = None 00120 00121 self._image_topic = image_topic 00122 self._image_stamp = image_stamp 00123 00124 wx.CallAfter(self.parent.Refresh) 00125 00126 def reset_size(self): 00127 with self._image_lock: 00128 if self._image: 00129 self.parent.ClientSize = self._image.size 00130 00131 ## Events 00132 00133 def on_size(self, event): 00134 with self._image_lock: 00135 self._image_surface = None 00136 00137 self.parent.Refresh() 00138 00139 def on_paint(self, event): 00140 dc = wx.lib.wxcairo.ContextFromDC(wx.PaintDC(self.parent)) 00141 00142 with self._image_lock: 00143 if not self._image: 00144 return 00145 00146 ix, iy, iw, ih = 0, 0, self.parent.ClientSize[0], self.parent.ClientSize[1] 00147 00148 # Rescale the bitmap if necessary 00149 if not self._image_surface: 00150 if self._image.size[0] != iw or self._image.size[1] != ih: 00151 self._image_surface = image_helper.pil_to_cairo(self._image.resize((iw, ih), Image.NEAREST)) 00152 else: 00153 self._image_surface = image_helper.pil_to_cairo(self._image) 00154 00155 # Draw bitmap 00156 dc.set_source_surface(self._image_surface, ix, iy) 00157 dc.rectangle(ix, iy, iw, ih) 00158 dc.fill() 00159 00160 # Draw overlay 00161 dc.set_font_size(self._overlay_font_size) 00162 font_height = dc.font_extents()[2] 00163 dc.set_source_rgb(*self._overlay_color) 00164 dc.move_to(self._overlay_indent[0], self._overlay_indent[1] + font_height) 00165 dc.show_text(bag_helper.stamp_to_str(self._image_stamp)) 00166 00167 def on_right_down(self, event): 00168 if self._image: 00169 self.parent.PopupMenu(ImagePopupMenu(self.parent, self), event.GetPosition()) 00170 00171 ## 00172 00173 def save_frame(self): 00174 if not self._image: 00175 return 00176 00177 file_spec = self.file_spec_tool.Value 00178 try: 00179 filename = file_spec % self._next_frame_num 00180 except Exception: 00181 wx.MessageDialog(None, 'Error with filename specification.\n\nPlease include a frame number format, e.g. frame%04d.png', 'Error', wx.OK | wx.ICON_ERROR).ShowModal() 00182 return 00183 00184 with self._image_lock: 00185 if self._image_msg: 00186 _save_image_msg(self._image_msg, filename) 00187 self._next_frame_num += 1 00188 00189 self.parent.SetStatusText('%s written' % filename) 00190 00191 def save_frame_as(self): 00192 if not self._image: 00193 return 00194 00195 dialog = wx.FileDialog(self.parent.Parent, 'Save frame to...', wildcard='PNG files (*.png)|*.png', style=wx.FD_SAVE) 00196 if dialog.ShowModal() == wx.ID_OK: 00197 with self._image_lock: 00198 if self._image_msg: 00199 _save_image_msg(self._image_msg, dialog.Path) 00200 dialog.Destroy() 00201 00202 def save_selected_frames(self): 00203 if not self._image or not self.timeline.has_selected_region: 00204 return 00205 00206 wx.CallAfter(self._show_export_dialog) 00207 00208 def _show_export_dialog(self): 00209 dialog = ExportFramesDialog(None, -1, 'Export frames', self.timeline, self._image_topic) 00210 dialog.Show() 00211 00212 """ 00213 def export_video(self): 00214 bag_file = self.timeline.bag_file 00215 00216 msg_positions = bag_index.msg_positions[self._image_topic] 00217 if len(msg_positions) == 0: 00218 return 00219 00220 dialog = wx.FileDialog(self.parent.Parent, 'Save video to...', wildcard='AVI files (*.avi)|*.avi', style=wx.FD_SAVE) 00221 if dialog.ShowModal() != wx.ID_OK: 00222 return 00223 video_filename = dialog.Path 00224 00225 import tempfile 00226 tmpdir = tempfile.mkdtemp() 00227 00228 frame_count = 0 00229 w, h = None, None 00230 00231 total_frames = len(bag_file.read_messages(self._image_topic, raw=True)) 00232 00233 for i, (topic, msg, t) in enumerate(bag_file.read_messages(self._image_topic)): 00234 img = image_helper.imgmsg_to_wx(msg) 00235 if img: 00236 frame_filename = '%s/frame-%s.png' % (tmpdir, str(t)) 00237 print '[%d / %d]' % (i + 1, total_frames) 00238 img.SaveFile(frame_filename, wx.BITMAP_TYPE_PNG) 00239 frame_count += 1 00240 00241 if w is None: 00242 w, h = img.GetWidth(), img.GetHeight() 00243 00244 if frame_count > 0: 00245 positions = numpy.array([stamp for (stamp, pos) in msg_positions]) 00246 if len(positions) > 1: 00247 spacing = positions[1:] - positions[:-1] 00248 fps = 1.0 / numpy.median(spacing) 00249 00250 print 'Encoding %dx%d at %d fps' % (w, h, fps) 00251 00252 try: 00253 command = ('mencoder', 00254 'mf://' + tmpdir + '/*.png', 00255 '-mf', 00256 'type=png:w=%d:h=%d:fps=%d' % (w, h, fps), 00257 '-ovc', 00258 'lavc', 00259 '-lavcopts', 00260 'vcodec=mpeg4', 00261 '-oac', 00262 'copy', 00263 '-o', 00264 video_filename) 00265 os.spawnvp(os.P_WAIT, 'mencoder', command) 00266 finally: 00267 shutil.rmtree(tmpdir) 00268 00269 dialog.Destroy() 00270 """ 00271 00272 class ImagePopupMenu(wx.Menu): 00273 def __init__(self, parent, image_view): 00274 wx.Menu.__init__(self) 00275 00276 self.image_view = image_view 00277 00278 # Reset Size 00279 reset_item = wx.MenuItem(self, wx.NewId(), 'Reset Size') 00280 self.AppendItem(reset_item) 00281 self.Bind(wx.EVT_MENU, lambda e: self.image_view.reset_size(), id=reset_item.Id) 00282 00283 # Save Frame... 00284 save_frame_as_item = wx.MenuItem(self, wx.NewId(), 'Save Frame...') 00285 self.AppendItem(save_frame_as_item) 00286 self.Bind(wx.EVT_MENU, lambda e: self.image_view.save_frame_as(), id=save_frame_as_item.Id) 00287 00288 # Save Selected Frames... 00289 save_selected_frames_item = wx.MenuItem(self, wx.NewId(), 'Save Selected Frames...') 00290 self.AppendItem(save_selected_frames_item) 00291 self.Bind(wx.EVT_MENU, lambda e: self.image_view.save_selected_frames(), id=save_selected_frames_item.Id) 00292 if not self.image_view.timeline.has_selected_region: 00293 save_selected_frames_item.Enable(False) 00294 00295 # Export to AVI... 00296 #export_video_item = wx.MenuItem(self, wx.NewId(), 'Export to AVI...') 00297 #self.AppendItem(export_video_item) 00298 #self.Bind(wx.EVT_MENU, lambda e: self.image_view.export_video(), id=export_video_item.Id) 00299 00300 class ExportFramesDialog(wx.Dialog): 00301 def __init__(self, parent, id, title, timeline, topic): 00302 wx.Dialog.__init__(self, parent, id, title, size=(280, 90)) 00303 00304 self.timeline = timeline 00305 self.topic = topic 00306 00307 panel = wx.Panel(self, -1) 00308 00309 self.file_spec_label = wx.StaticText(panel, -1, 'File spec:', (5, 10)) 00310 self.file_spec_text = wx.TextCtrl (panel, -1, 'frame%04d.png', (65, 7), (210, 22)) 00311 00312 self.steps_label = wx.StaticText(panel, -1, 'Every:', (5, 35)) 00313 self.steps_text = wx.lib.masked.NumCtrl(panel, -1, pos=(65, 32), size=(34, 22), value=1, integerWidth=3, allowNegative=False) 00314 self.steps_text.SetMin(1) 00315 00316 self.frames_label = wx.StaticText(panel, -1, 'frame(s)', (102, 36)) 00317 00318 self.export_button = wx.Button(self, -1, 'Export', size=(68, 26)) 00319 self.cancel_button = wx.Button(self, -1, 'Cancel', size=(68, 26)) 00320 00321 hbox = wx.BoxSizer(wx.HORIZONTAL) 00322 hbox.Add(self.export_button, 1) 00323 hbox.Add(self.cancel_button, 1, wx.LEFT, 5) 00324 00325 vbox = wx.BoxSizer(wx.VERTICAL) 00326 vbox.Add(panel) 00327 vbox.Add(hbox, 1, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 5) 00328 self.SetSizer(vbox) 00329 00330 self.Bind(wx.EVT_BUTTON, self._on_export, id=self.export_button.Id) 00331 self.Bind(wx.EVT_BUTTON, self._on_cancel, id=self.cancel_button.Id) 00332 00333 self.progress = None 00334 self.export_thread = None 00335 00336 def _on_export(self, event): 00337 file_spec = self.file_spec_text.Value 00338 try: 00339 test_filename = file_spec % 0 00340 except Exception: 00341 wx.MessageDialog(None, 'Error with filename specification.\n\nPlease include a frame number format, e.g. frame%04d.png', 'Error', wx.OK | wx.ICON_ERROR).ShowModal() 00342 return 00343 00344 if not self.timeline.start_background_task('Exporting frames to "%s"' % file_spec): 00345 return 00346 00347 wx.CallAfter(wx.GetApp().GetTopWindow().StatusBar.gauge.Show) 00348 00349 try: 00350 step = max(1, int(self.steps_text.Value)) 00351 except ValueError: 00352 step = 1 00353 00354 bag_entries = list(self.timeline.get_entries_with_bags(self.topic, self.timeline.play_region[0], self.timeline.play_region[1]))[::step] 00355 total_frames = len(bag_entries) 00356 00357 if not self.timeline.background_task_cancel: 00358 self.export_thread = threading.Thread(target=self._run, args=(file_spec, bag_entries)) 00359 self.export_thread.setDaemon(True) 00360 self.export_thread.start() 00361 00362 self.Close() 00363 00364 def _run(self, file_spec, bag_entries): 00365 total_frames = len(bag_entries) 00366 00367 progress = 0 00368 00369 def update_progress(v): 00370 wx.GetApp().TopWindow.StatusBar.progress = v 00371 00372 frame_num = 1 00373 for i, (bag, entry) in enumerate(bag_entries): 00374 if self.timeline.background_task_cancel: 00375 break 00376 00377 try: 00378 topic, msg, t = self.timeline.read_message(bag, entry.position) 00379 00380 filename = file_spec % frame_num 00381 try: 00382 _save_image_msg(msg, filename) 00383 frame_num += 1 00384 except Exception, ex: 00385 print >> sys.stderr, 'Error saving frame at %s to disk: %s' % (str(t), str(ex)) 00386 00387 new_progress = int(100.0 * (float(frame_num) / total_frames)) 00388 if new_progress != progress: 00389 progress = new_progress 00390 wx.CallAfter(update_progress, progress) 00391 except Exception, ex: 00392 print >> sys.stderr, 'Error saving frame %d: %s' % (i, str(ex)) 00393 00394 wx.CallAfter(wx.GetApp().TopWindow.StatusBar.gauge.Hide) 00395 00396 wx.CallAfter(self.Destroy) 00397 00398 self.timeline.stop_background_task() 00399 00400 def _on_cancel(self, event): 00401 self.Destroy() 00402 00403 def _save_image_msg(img_msg, filename): 00404 pil_img = image_helper.imgmsg_to_pil(img_msg, rgba=False) 00405 00406 if pil_img.mode in ('RGB', 'RGBA'): 00407 pil_img = pil_img.convert('RGB') 00408 pil_img = image_helper.pil_bgr2rgb(pil_img) 00409 pil_img = pil_img.convert('RGB') 00410 00411 pil_img.save(filename)