|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +''' |
| 4 | +Picture Viewer Window |
| 5 | +
|
| 6 | +Displays a window for users to review a collection of images quickly |
| 7 | +
|
| 8 | +AP_FLAKE8_CLEAN |
| 9 | +''' |
| 10 | + |
| 11 | +from threading import Thread |
| 12 | +from math import ceil |
| 13 | +import cv2 |
| 14 | +import time |
| 15 | +import os |
| 16 | +import numpy as np |
| 17 | +import wx.lib.scrolledpanel as scrolled |
| 18 | +from MAVProxy.modules.lib import mp_util |
| 19 | +if mp_util.has_wxpython: |
| 20 | + from MAVProxy.modules.lib.wx_loader import wx |
| 21 | + from MAVProxy.modules.lib.mp_menu import MPMenuTop |
| 22 | + from MAVProxy.modules.lib.mp_menu import MPMenuItem |
| 23 | + from MAVProxy.modules.lib.mp_menu import MPMenuSubMenu |
| 24 | + from MAVProxy.modules.lib.mp_image import MPImage, MPImagePanel |
| 25 | + from MAVProxy.modules.lib.mp_menu import MPMenuCallDirDialog |
| 26 | + |
| 27 | + |
| 28 | +class mosaic_window2: |
| 29 | + """displays a mosaic of images""" |
| 30 | + |
| 31 | + def __init__(self, filelist): |
| 32 | + |
| 33 | + # determine if filelist is a string or a list of strings |
| 34 | + self.filenumber = 0 |
| 35 | + if type(filelist) is str: |
| 36 | + self.filelist = [] |
| 37 | + self.filelist.append(filelist) |
| 38 | + else: |
| 39 | + # use the first item in the list |
| 40 | + self.filelist = filelist |
| 41 | + |
| 42 | + # hardcoded thumbnail image size and number of columns |
| 43 | + self.thumb_size = 100 |
| 44 | + self.thumb_columns = 5 |
| 45 | + self.thumb_rows = ceil(len(filelist) / self.thumb_columns) |
| 46 | + |
| 47 | + # create image viewer |
| 48 | + self.im = None |
| 49 | + #self.update_image() |
| 50 | + |
| 51 | + # create window |
| 52 | + self.app = wx.App() |
| 53 | + self.frame = wx.Frame(None, title="Mosaic", size=(650, 200)) |
| 54 | + self.frame.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) |
| 55 | + |
| 56 | + # add menu |
| 57 | + self.menu = wx.Menu() |
| 58 | + self.menu.Append(1, "Open Folder", "Open a Folder of images") |
| 59 | + self.menu_bar = wx.MenuBar() |
| 60 | + self.menu_bar.Append(self.menu, "Menu") |
| 61 | + self.frame.SetMenuBar(self.menu_bar) |
| 62 | + #self.frame.Bind(wx.EVT_MENU, self.menu_set_api_key_show, id=1) |
| 63 | + |
| 64 | + # add settings input window |
| 65 | + self.settings_frame = wx.Frame(None, title="Input Settings", size=(560, 50)) |
| 66 | + self.settings_text_input = wx.TextCtrl(self.settings_frame, id=-1, pos=(10, 10), size=(450, -1), |
| 67 | + style=wx.TE_PROCESS_ENTER, value="hello") |
| 68 | + self.settings_set_button = wx.Button(self.settings_frame, id=-1, label="Set", pos=(470, 10), size=(75, 25)) |
| 69 | + #self.settings_frame.Bind(wx.EVT_BUTTON, self.settings_set_button_click, self.settings_set_button) |
| 70 | + #self.settings_frame.Bind(wx.EVT_TEXT_ENTER, self.settings_set_button_click, self.settings_text_input) |
| 71 | + #self.settings_frame.Bind(wx.EVT_CLOSE, self.apikey_close_button_click) |
| 72 | + |
| 73 | + # add a scrolled panel |
| 74 | + self.scrolled_panel = scrolled.ScrolledPanel(self.frame, -1, size=(600, 600), style=wx.TAB_TRAVERSAL) |
| 75 | + self.scrolled_panel_sizer = wx.GridSizer(cols=5, hgap=5, vgap=5) |
| 76 | + |
| 77 | + # add an image |
| 78 | + #img1_path = self.filelist[1] |
| 79 | + #self.wx_image1 = wx.Image(img1_path, wx.BITMAP_TYPE_ANY).Scale(100, 100) |
| 80 | + #self.image = wx.StaticBitmap(self.scrolled_panel, wx.ID_ANY, wx.Bitmap(self.wx_image1)) |
| 81 | + |
| 82 | + #img2_path = self.filelist[2] |
| 83 | + #self.wx_image2 = wx.Image(img2_path, wx.BITMAP_TYPE_ANY).Scale(100, 100) |
| 84 | + #self.image2 = wx.StaticBitmap(self.scrolled_panel, wx.ID_ANY, wx.Bitmap(self.wx_image2)) |
| 85 | + |
| 86 | + #self.scrolled_panel_sizer.Add(self.image, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) |
| 87 | + #self.scrolled_panel_sizer.Add(self.image2, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) |
| 88 | + |
| 89 | + # add images |
| 90 | + for i in range(len(self.filelist)): |
| 91 | + img_path = self.filelist[i] |
| 92 | + wx_image = wx.Image(img_path, wx.BITMAP_TYPE_ANY).Scale(100, 100) |
| 93 | + image = wx.StaticBitmap(self.scrolled_panel, wx.ID_ANY, wx.Bitmap(wx_image)) |
| 94 | + self.scrolled_panel_sizer.Add(image, proportion=0, flag=wx.EXPAND | wx.ALL, border=2) |
| 95 | + |
| 96 | + self.scrolled_panel.SetSizer(self.scrolled_panel_sizer) |
| 97 | + self.scrolled_panel.SetupScrolling(scroll_x=True, scroll_y=True) |
| 98 | + |
| 99 | + # add a read-only reply text box |
| 100 | + self.text_reply = wx.TextCtrl(self.frame, id=-1, size=(600, 80), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH) |
| 101 | + |
| 102 | + # add a cancel button |
| 103 | + self.cancel_button = wx.Button(self.frame, id=-1, label="cancel", size=(75, 25)) |
| 104 | + #self.frame.Bind(wx.EVT_BUTTON, self.cancel_button_click , self.cancel_button) |
| 105 | + |
| 106 | + # add a vertical and horizontal sizers |
| 107 | + self.vert_sizer = wx.BoxSizer(wx.VERTICAL) |
| 108 | + self.horiz_sizer = wx.BoxSizer(wx.HORIZONTAL) |
| 109 | + |
| 110 | + self.horiz_sizer.Add(self.cancel_button, proportion=0, flag=wx.ALIGN_TOP | wx.ALL, border=5) |
| 111 | + wx.CallAfter(self.cancel_button.Disable) |
| 112 | + |
| 113 | + # set size hints and add sizer to frame |
| 114 | + self.vert_sizer.Add(self.scrolled_panel, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) |
| 115 | + self.vert_sizer.Add(self.text_reply, proportion=0, flag=wx.EXPAND, border=5) |
| 116 | + #self.vert_sizer.Add(self.image, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) |
| 117 | + #self.vert_sizer.Add(self.image2, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) |
| 118 | + self.vert_sizer.Add(self.horiz_sizer, proportion=0, flag=wx.EXPAND) |
| 119 | + #self.frame.Bind(wx.EVT_SIZE, self.on_resize) |
| 120 | + self.frame.SetSizer(self.vert_sizer) |
| 121 | + self.frame.Layout() |
| 122 | + |
| 123 | + # set focus on the input text box |
| 124 | + self.text_reply.SetFocus() |
| 125 | + |
| 126 | + # show frame |
| 127 | + self.frame.Show() |
| 128 | + |
| 129 | + # window loop (this does not return until the window is closed) |
| 130 | + self.app.MainLoop() |
| 131 | + |
| 132 | + self.thread = Thread(target=self.mosaic_window_loop, name='mosaic_window_loop') |
| 133 | + self.thread.daemon = False |
| 134 | + self.thread.start() |
| 135 | + |
| 136 | + # main loop |
| 137 | + #def mosaic_window_loop(self): |
| 138 | + # """main thread""" |
| 139 | + # while True: |
| 140 | + # if self.im is None: |
| 141 | + # break |
| 142 | + # time.sleep(0.25) |
| 143 | + # self.check_events() |
| 144 | + |
| 145 | + # set window title |
| 146 | + def set_title(self, title): |
| 147 | + """set image title""" |
| 148 | + if self.im is None: |
| 149 | + return |
| 150 | + self.im.set_title(title) |
| 151 | + |
| 152 | + # process window events |
| 153 | + def check_events(self): |
| 154 | + """check for image events""" |
| 155 | + if self.im is None: |
| 156 | + return |
| 157 | + if not self.im.is_alive(): |
| 158 | + self.im = None |
| 159 | + return |
| 160 | + for event in self.im.events(): |
| 161 | + if isinstance(event, MPMenuItem): |
| 162 | + if event.returnkey == "openfolder": |
| 163 | + self.cmd_openfolder() |
| 164 | + elif event.returnkey == "fitWindow": |
| 165 | + print("fitting to window") |
| 166 | + self.im.fit_to_window() |
| 167 | + elif event.returnkey == "fullSize": |
| 168 | + print("full size") |
| 169 | + self.im.full_size() |
| 170 | + elif event.returnkey == "nextimage": |
| 171 | + self.cmd_nextimage() |
| 172 | + elif event.returnkey == "previmage": |
| 173 | + self.cmd_previmage() |
| 174 | + else: |
| 175 | + debug_str = "event: %s" % event |
| 176 | + self.set_title(debug_str) |
| 177 | + continue |
| 178 | + if event.ClassName == "wxMouseEvent": |
| 179 | + if event.X is not None and event.Y is not None: |
| 180 | + print("mosaic pixel x:%f y:%f" % (event.X, event.Y)) |
| 181 | + |
| 182 | + # display dialog to open a folder |
| 183 | + def cmd_openfolder(self): |
| 184 | + print("I will open a folder") |
| 185 | + |
| 186 | + # display dialog to open a file |
| 187 | + def cmd_openfile(self): |
| 188 | + print("I will open a file") |
| 189 | + |
| 190 | + # update current image to next image |
| 191 | + def cmd_nextimage(self): |
| 192 | + if self.filenumber >= len(self.filelist)-1: |
| 193 | + print("picviewer: already at last image %d" % self.filenumber) |
| 194 | + return |
| 195 | + self.filenumber = self.filenumber+1 |
| 196 | + self.update_image() |
| 197 | + |
| 198 | + # update current image to previous image |
| 199 | + def cmd_previmage(self): |
| 200 | + if self.filenumber <= 0: |
| 201 | + print("picviewer: already at first image") |
| 202 | + return |
| 203 | + self.filenumber = self.filenumber - 1 |
| 204 | + self.update_image() |
| 205 | + |
| 206 | + # update the mosaic of images |
| 207 | + # should be called if filenumber is changed |
| 208 | + def update_image(self): |
| 209 | + # update filename |
| 210 | + self.filename = self.filelist[self.filenumber] |
| 211 | + base_filename = os.path.basename(self.filename) |
| 212 | + |
| 213 | + # create image viewer if required |
| 214 | + if self.im is None: |
| 215 | + self.im = MPImage(title=base_filename, |
| 216 | + mouse_events=True, |
| 217 | + mouse_movement_events=True, |
| 218 | + key_events=True, |
| 219 | + can_drag=True, |
| 220 | + can_zoom=False, |
| 221 | + auto_size=False, |
| 222 | + auto_fit=False) |
| 223 | + |
| 224 | + # check if image viewer was created |
| 225 | + if self.im is None: |
| 226 | + print("picviewer: failed to create image viewer") |
| 227 | + return |
| 228 | + |
| 229 | + # set title to filename |
| 230 | + self.set_title("Mosaic " + base_filename) |
| 231 | + |
| 232 | + # create blank image |
| 233 | + temp_image = cv2.imread(self.filename) |
| 234 | + h, w, c = temp_image.shape |
| 235 | + mosaic_image = 255 * np.ones(shape=(self.thumb_rows * self.thumb_size, |
| 236 | + self.thumb_columns * self.thumb_size, c), |
| 237 | + dtype=np.uint8) |
| 238 | + |
| 239 | + # iterate through images and add thumbnails to mosaic |
| 240 | + row = 0 |
| 241 | + col = 0 |
| 242 | + for i in range(len(self.filelist)): |
| 243 | + image_filename = self.filelist[i] |
| 244 | + image = cv2.imread(image_filename) |
| 245 | + image_small = cv2.resize(image, (self.thumb_size, self.thumb_size), interpolation=cv2.INTER_AREA) |
| 246 | + self.overlay_image(mosaic_image, image_small, col * self.thumb_size, row * self.thumb_size) |
| 247 | + col = col + 1 |
| 248 | + if col >= self.thumb_columns: |
| 249 | + col = 0 |
| 250 | + row = row + 1 |
| 251 | + |
| 252 | + # update image and colormap |
| 253 | + self.im.set_image(mosaic_image) |
| 254 | + self.im.set_colormap("None") |
| 255 | + |
| 256 | + def overlay_image(self, img, img2, x, y): |
| 257 | + '''overlay a 2nd image on a first image, at position x,y on the first image''' |
| 258 | + (img_width, img_height) = self.image_shape(img2) |
| 259 | + img[y:y+img_height, x:x+img_width] = img2 |
| 260 | + |
| 261 | + def image_shape(self, img): |
| 262 | + '''return (w,h) of an image, coping with different image formats''' |
| 263 | + height, width = img.shape[:2] |
| 264 | + return (width, height) |
0 commit comments