Skip to content

Commit 3cdb8af

Browse files
committed
mavpicviewer: image review tool
1 parent b9e9133 commit 3cdb8af

File tree

7 files changed

+651
-0
lines changed

7 files changed

+651
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
cd ..\..\
2+
python setup.py build install --user
3+
python .\MAVProxy\tools\mavpicviewer\mavpicviewer.py
4+
pause
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
3+
'''
4+
MAV Picture Viewer
5+
6+
Quick and efficient reviewing of images taken from a drone
7+
8+
AP_FLAKE8_CLEAN
9+
'''
10+
11+
import os
12+
from argparse import ArgumentParser
13+
from pathlib import Path
14+
from MAVProxy.modules.lib import multiproc
15+
import picviewer_window
16+
17+
prefix_str = "mavpicviewer: "
18+
19+
20+
class mavpicviewer():
21+
22+
# constructor
23+
def __init__(self):
24+
self.picviewer_window = None
25+
26+
# display dialog to open a folder
27+
def cmd_openfolder(self, folderpath):
28+
file_list = self.file_list(folderpath, ['jpg', 'jpeg'])
29+
if file_list is None or not file_list:
30+
print("picviewer: no files found")
31+
return
32+
self.picviewer_window = picviewer_window.picviewer_window(file_list)
33+
34+
# open picture viewer to display a single file
35+
def cmd_openfile(self, filepath):
36+
# check file exists
37+
if not Path(filepath).exists():
38+
print("picviewer: %s not found" % filepath)
39+
return
40+
filelist = []
41+
filelist.append(filepath)
42+
self.picviewer_window = picviewer_window.picviewer_window(filelist)
43+
44+
# return an array of files for a given directory and extension
45+
def file_list(self, directory, extensions):
46+
'''return file list for a directory'''
47+
flist = []
48+
for filename in os.listdir(directory):
49+
extension = filename.split('.')[-1]
50+
if extension.lower() in extensions:
51+
flist.append(os.path.join(directory, filename))
52+
sorted_list = sorted(flist, key=str.lower)
53+
return sorted_list
54+
55+
56+
# main function
57+
if __name__ == "__main__":
58+
multiproc.freeze_support()
59+
parser = ArgumentParser(description=__doc__)
60+
parser.add_argument("filepath", default=".", help="filename or directory holding images")
61+
args = parser.parse_args()
62+
63+
# check destination directory exists
64+
if not os.path.exists(args.filepath):
65+
exit(prefix_str + "invalid destination directory")
66+
67+
# check if file or directory
68+
if os.path.isfile(args.filepath):
69+
picviewer = mavpicviewer()
70+
picviewer.cmd_openfile(args.filepath)
71+
else:
72+
picviewer = mavpicviewer()
73+
picviewer.cmd_openfolder(args.filepath)
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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+
from MAVProxy.modules.lib import mp_util
18+
if mp_util.has_wxpython:
19+
from MAVProxy.modules.lib.mp_menu import MPMenuTop
20+
from MAVProxy.modules.lib.mp_menu import MPMenuItem
21+
from MAVProxy.modules.lib.mp_menu import MPMenuSubMenu
22+
from MAVProxy.modules.lib.mp_image import MPImage
23+
from MAVProxy.modules.lib.mp_menu import MPMenuCallDirDialog
24+
25+
26+
class mosaic_window:
27+
"""displays a mosaic of images"""
28+
29+
def __init__(self, filelist):
30+
31+
# determine if filelist is a string or a list of strings
32+
self.filenumber = 0
33+
if type(filelist) is str:
34+
self.filelist = []
35+
self.filelist.append(filelist)
36+
else:
37+
# use the first item in the list
38+
self.filelist = filelist
39+
40+
# hardcoded thumbnail image size and number of columns
41+
self.thumb_size = 100
42+
self.thumb_columns = 5
43+
self.thumb_rows = ceil(len(filelist) / self.thumb_columns)
44+
45+
# create image viewer
46+
self.im = None
47+
self.update_image()
48+
49+
# create menu
50+
self.menu = None
51+
if mp_util.has_wxpython:
52+
self.menu = MPMenuTop([MPMenuSubMenu('&File', items=[MPMenuItem(name='&Open\tCtrl+O', returnkey='openfolder',
53+
handler=MPMenuCallDirDialog(title='Open Folder')),
54+
MPMenuItem('&Save\tCtrl+S'),
55+
MPMenuItem('Close', 'Close'),
56+
MPMenuItem('&Quit\tCtrl+Q', 'Quit')])])
57+
self.im.set_menu(self.menu)
58+
59+
popup = self.im.get_popup_menu()
60+
popup.add_to_submenu(["Mode"], MPMenuItem("ClickTrack", returnkey="Mode:ClickTrack"))
61+
popup.add_to_submenu(["Mode"], MPMenuItem("Flag", returnkey="Mode:Flag"))
62+
63+
self.thread = Thread(target=self.mosaic_window_loop, name='mosaic_window_loop')
64+
self.thread.daemon = False
65+
self.thread.start()
66+
67+
# main loop
68+
def mosaic_window_loop(self):
69+
"""main thread"""
70+
while True:
71+
if self.im is None:
72+
break
73+
time.sleep(0.25)
74+
self.check_events()
75+
76+
# set window title
77+
def set_title(self, title):
78+
"""set image title"""
79+
if self.im is None:
80+
return
81+
self.im.set_title(title)
82+
83+
# process window events
84+
def check_events(self):
85+
"""check for image events"""
86+
if self.im is None:
87+
return
88+
if not self.im.is_alive():
89+
self.im = None
90+
return
91+
for event in self.im.events():
92+
if isinstance(event, MPMenuItem):
93+
if event.returnkey == "openfolder":
94+
self.cmd_openfolder()
95+
elif event.returnkey == "fitWindow":
96+
print("fitting to window")
97+
self.im.fit_to_window()
98+
elif event.returnkey == "fullSize":
99+
print("full size")
100+
self.im.full_size()
101+
elif event.returnkey == "nextimage":
102+
self.cmd_nextimage()
103+
elif event.returnkey == "previmage":
104+
self.cmd_previmage()
105+
else:
106+
debug_str = "event: %s" % event
107+
self.set_title(debug_str)
108+
continue
109+
if event.ClassName == "wxMouseEvent":
110+
if event.X is not None and event.Y is not None:
111+
print("mosaic pixel x:%f y:%f" % (event.X, event.Y))
112+
113+
# display dialog to open a folder
114+
def cmd_openfolder(self):
115+
print("I will open a folder")
116+
117+
# display dialog to open a file
118+
def cmd_openfile(self):
119+
print("I will open a file")
120+
121+
# update current image to next image
122+
def cmd_nextimage(self):
123+
if self.filenumber >= len(self.filelist)-1:
124+
print("picviewer: already at last image %d" % self.filenumber)
125+
return
126+
self.filenumber = self.filenumber+1
127+
self.update_image()
128+
129+
# update current image to previous image
130+
def cmd_previmage(self):
131+
if self.filenumber <= 0:
132+
print("picviewer: already at first image")
133+
return
134+
self.filenumber = self.filenumber - 1
135+
self.update_image()
136+
137+
# update the mosaic of images
138+
# should be called if filenumber is changed
139+
def update_image(self):
140+
# update filename
141+
self.filename = self.filelist[self.filenumber]
142+
base_filename = os.path.basename(self.filename)
143+
144+
# create image viewer if required
145+
if self.im is None:
146+
self.im = MPImage(title=base_filename,
147+
mouse_events=True,
148+
mouse_movement_events=True,
149+
key_events=True,
150+
can_drag=True,
151+
can_zoom=False,
152+
auto_size=False,
153+
auto_fit=False)
154+
155+
# check if image viewer was created
156+
if self.im is None:
157+
print("picviewer: failed to create image viewer")
158+
return
159+
160+
# set title to filename
161+
self.set_title("Mosaic " + base_filename)
162+
163+
# create blank image
164+
temp_image = cv2.imread(self.filename)
165+
h, w, c = temp_image.shape
166+
mosaic_image = 255 * np.ones(shape=(self.thumb_rows * self.thumb_size,
167+
self.thumb_columns * self.thumb_size, c),
168+
dtype=np.uint8)
169+
170+
# iterate through images and add thumbnails to mosaic
171+
row = 0
172+
col = 0
173+
for i in range(len(self.filelist)):
174+
image_filename = self.filelist[i]
175+
image = cv2.imread(image_filename)
176+
image_small = cv2.resize(image, (self.thumb_size, self.thumb_size), interpolation=cv2.INTER_AREA)
177+
self.overlay_image(mosaic_image, image_small, col * self.thumb_size, row * self.thumb_size)
178+
col = col + 1
179+
if col >= self.thumb_columns:
180+
col = 0
181+
row = row + 1
182+
183+
# update image and colormap
184+
self.im.set_image(mosaic_image)
185+
self.im.set_colormap("None")
186+
187+
def overlay_image(self, img, img2, x, y):
188+
'''overlay a 2nd image on a first image, at position x,y on the first image'''
189+
(img_width, img_height) = self.image_shape(img2)
190+
img[y:y+img_height, x:x+img_width] = img2
191+
192+
def image_shape(self, img):
193+
'''return (w,h) of an image, coping with different image formats'''
194+
height, width = img.shape[:2]
195+
return (width, height)

0 commit comments

Comments
 (0)