-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
487 lines (435 loc) · 19.4 KB
/
Copy pathmain.py
File metadata and controls
487 lines (435 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
import os
import shutil
import subprocess
import sys
from ttkbootstrap import Style
from tkinter import simpledialog, PhotoImage, Frame, Tk
from pathlib import Path
from threading import Thread
import scripts.PhotoManager as pm
from gui.PipelineGUI import PipelineGUI
from gui.StartGUI import StartGUI
from scripts.ReconManager import ReconManager
import pickle
import ctypes
import scripts.PointCloudManager as PointCloudManager
import scripts.RecentsManager as RecentsManager
# VirtualRocks source is released under GPL-3.0-only or GPL-3.0-or-later
# DEBUG = True will cause the application to skip over recon scripts for testing
DEBUG = False
# Progress Constants
STARTED = 0
PHOTOS = 10
MATCHER = 70
MESHER = 100
# Path to specific python version installed by the installer
PYTHONPATH = os.getenv('LOCALAPPDATA') + "\\Programs\\Python\\Python311\\python.exe"
class main(Tk):
def __init__(self, *args, **kwargs):
"""
Main tkinter application, started with mainloop(). Main is the controller
for the **VirtualRocks** project and manages app state as well as communicates
between the reconstruction of the 3D point cloud/mesh and the GUI.
Args:
arg[0] (str): Optional path to a .vrp file.
"""
Tk.__init__(self, *args, **kwargs)
# Controller Variables
self.projdir = None
self.imgdir = ""
self.recon = None
self.state = STARTED
self.fullscreen = False
# Sets app icon and identifier (needed for app icon)
self.myappid = u'o7.VirtualRocks.PipelineApp.version-1.0' # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(self.myappid)
# Configuration variables
self.projectname = "project"
self.picklepath = None
self.minsize(500, 300)
centerdim = self._open_middle(1000,700)
self.geometry('%dx%d+%d+%d' % (1000, 700, centerdim[0], centerdim[1]))
self.title("VirtualRocks")
icon = PhotoImage(file=Path(r"gui\placeholder\logo.png").resolve())
self.iconphoto(True, icon)
# Application styling
self.style = Style("darkly")
self.styleflag = "dark"
self.init_style()
# container is a stack of frames (aka our two main pages)
self.container = Frame(self)
self.container.pack(side="top", fill="both", expand=True)
self.container.grid_rowconfigure(0, weight=1)
self.container.grid_columnconfigure(0, weight=1)
if not args[0]:
# Load staring page and start application
self.page1 = StartGUI(parent=self.container, controller=self)
self.page1.grid(row=0, column=0, sticky="nsew")
self.page1.tkraise()
else:
# Open project directly
self.open_project(args[0])
# Binding for fullscreen toggle
self.bind("<F11>", self._toggle_fullscreen)
self.bind("<Escape>", self._end_fullscreen)
self.bind("<Control-z>", self.restore)
def _startup(self):
"""
Helper function with common startup tasks for both opening and creating
a new project. Will be called by `new_project` and `open_project`.
"""
self.page2 = PipelineGUI(self.container, self, self.projdir)
self.page2.grid(row=0, column=0, sticky="nsew")
self.page2.set_example_image(self.page2.DEFAULT_PREVIEW)
self.page2.tkraise()
self.recon = ReconManager(self, self.projdir)
self.page2.menubar.entryconfig("Reconstruction", state="normal")
RecentsManager.add(self.picklepath)
def init_style(self):
"""
Handles assignment of style elements when app starts or when the style changes. This
includes the button size, the title font size on the start page, and progress bar
appearance. The progress on the current step, if one is running, is also reprinted to the
bar.
"""
# setting initial style stuff
self.style.configure("TButton", width=16)
self.style.configure("cancel.TButton", width=30)
self.style.configure("title.TLabel", font=('Helvetica', 30, "bold"))
# Progress bar styling
self.style.layout("prog.Horizontal.TProgressbar",
[('Horizontal.Progressbar.trough',
{'children': [('Horizontal.Progressbar.pbar', {'side': 'left', 'sticky': 'ns'})],
'sticky': 'nswe'}),
('Horizontal.Progressbar.label', {'sticky': ''})])
self.style.configure("prog.Horizontal.TProgressbar", font=('Helvetica', 11), background="goldenrod1")
#need to add the progress bar update stuff here.
currentprogress = self._get_progress()
if 0 < currentprogress < 100:
self.style.configure('prog.Horizontal.TProgressbar', text='{:g} %'.format(currentprogress))
def new_project(self, projdir, name=None, imgdir=None):
"""
Method for creating a new project. Will create a .vrp file after prompting
the user for a project name and workspace directory. After creating a fresh
project, app will launch into the main page of the app.
Args:
projdir (pathlib.Path): project directory to save .pkl file in.
name (str): optional, the name of the project.
imgdir (pathlib.Path): optional, image directory
"""
print("creating new project")
self.projdir = Path(projdir)
if not name:
self.projectname = simpledialog.askstring(title="Name Project As...", prompt="Enter a name for this project:", parent=self.page1, initialvalue=self.projectname)
else:
self.projectname = name
self.picklepath = self.projdir / Path(self.projectname + '.vrp')
self._startup()
self.page2.dirtext.config(text=f"Workspace: [ {self.projdir} ]")
self.title("VirtualRocks: " + self.projectname)
if imgdir:
self.imgdir = imgdir
numimg = pm.get_num_img(imgdir)
self.page2.update_text(numimg)
self.page2.set_example_image(self.imgdir / Path(pm.get_example_img(imgdir)))
if numimg > 0:
self.update_state(PHOTOS)
else:
self.update_state(STARTED)
def open_project(self, projfile):
"""
Method for opening an existing VirtualRocks project. Will load the correct
application state and paths from the .vrp file and launch into the main page
of the app.
Args:
projdir (pathlib.Path): Project directory containing .pkl file.
"""
print("opening project")
self.picklepath = projfile
self.projectname = Path(projfile).stem
# Load the path variables from the file
self.projdir = Path(projfile).parent
with open(projfile, 'rb') as file:
(path,self.state) = pickle.load(file)
if Path(path).is_absolute():
self.imgdir = path
else:
self.imgdir = self.projdir / path
self._startup()
self.update_state(self.state)
self.title("VirtualRocks: " + self.projectname)
self.page2.dirtext.config(text=f"Workspace: [ {self.projdir} ]")
try:
self.page2.update_text(pm.get_num_img(self.imgdir))
self.page2.set_example_image(self.imgdir / Path(pm.get_example_img(self.imgdir)))
except Exception as e:
self.page2.log(str(e))
self.page2.log("Could not find image directory")
self.update_state(STARTED)
def back_to_start(self):
"""
Handler method for reopening the starting page when the "Back to Start" menu item in the
`File` menu tab in :ref:`AppWindow <appwindow>` is pressed. It reopens the Tk Frame made by
:ref:`StartGUI <startGUI>`, resets the app title, and disables the `Reconstruction` menu
tab.
"""
self.page1.tkraise()
self.page2.menubar.entryconfig("Reconstruction", state="disabled")
self.title("VirtualRocks")
def add_photos(self, imgdir):
"""
Method that sets the controller variable for image directory and sets the example image
from pictures in the selected image directory. Progress on this step is tracked using the
`_send_log` helper function in :ref:`ReconManager <reconmanager>`.
The handler method `photos_handler` in :ref:`PipelineGUI <pipelineGUI>` opens a dialog
that allows the user to select an image directory.
Args:
imgdir (pathlib.Path): path to the image directory
"""
self.recon._send_log("$$")
self.recon._send_log("$.Image Loading.0$")
self.projdir.resolve()
self.imgdir = Path(imgdir).resolve()
self.recon._send_log("$Image Loading..100$")
numimg = pm.get_num_img(self.imgdir)
self.page2.update_text(numimg)
self.page2.set_example_image(self.imgdir / Path(pm.get_example_img(self.imgdir)))
if numimg > 0:
self.update_state(PHOTOS)
def set_bounds(self, minx, maxx, miny, maxy, minz, maxz):
"""
Method communicates between the GUI and the PointCloudManager for trimming models by
removing points from the dense point clouds.
This step automatically completes, and its progress is tracked using the `_send_log`
helper function in :ref:`ReconManager <reconmanager>`.
The handler method `bounds_handler` in :ref:`PipelineGUI <pipelineGUI>` creates the dialog
(using the :ref:`BoundsDialog <boundsdialog>` class) and passes the bounds received from
the user to this method. Bounds are inclusive.
Args:
minx (float): minimum x axis bound.
maxx (float): maximum x axis bound.
miny (float): minimum y axis bound.
maxy (float): maximum y axis bound.
minz (float): minimum z axis bound.
maxz (float): maximum z axis bound.
"""
self.recon._send_log("$$")
self.recon._send_log("$Trimming Bounds..100$")
dense = Path(self.projdir / "dense")
PointCloudManager.remove_points(Path(dense / "fused.ply"), minx, maxx, miny, maxy, minz, maxz)
self.page2.log("Trimming complete")
try:
PointCloudManager.create_heat_map(Path(dense / "fused.ply"), dense)
PointCloudManager.create_height_map(Path(dense / "fused.ply"), dense)
except Exception as e:
self.page2.log(str(e))
self.page2.log("No Points Found")
shutil.copy(self.page2.DEFAULT_CHART, Path(self.projdir) / dense / "height_map.png")
shutil.copy(self.page2.DEFAULT_CHART, Path(self.projdir) / dense / "heat_map.png")
if self.page2.viewtype:
self.page2.set_chart(Path(dense/ "height_map.png"))
else:
self.page2.set_chart(Path(dense/ "heat_map.png"))
def restore(self):
"""
Handler method for the "Reset" button on the setting bounds step in
:ref:`PipelineGUI <pipelineGUI>`. It overwrites the current `fused.ply` file with the
unedited `save.ply` copy. The method undoes any previous point cloud trims done when
setting bounds.
"""
if self.state >= MATCHER:
self.page2.log("Point cloud restoration complete")
dense = Path(self.projdir / "dense")
savefile = Path(dense / "save.ply")
shutil.copy(savefile, Path(dense / "fused.ply"))
PointCloudManager.create_heat_map(Path(dense / "fused.ply"), dense)
PointCloudManager.create_height_map(Path(dense / "fused.ply"), dense)
if self.page2.viewtype:
self.page2.set_chart(Path(dense/ "height_map.png"))
else:
self.page2.set_chart(Path(dense/ "heat_map.png"))
else:
self.page2.log("Nothing to restore, run matcher to create a point cloud")
def start_matcher(self):
"""
Handler method for starting the mesher (:ref:`Colmap <colmap>`), called via "2: Matcher"
button press in :ref:`PipelineGUI <pipelineGUI>`.
It starts a new thread for the `matcher` method in the :ref:`ReconManager <reconmanager>`
class.
"""
self.recon.imgdir = self.imgdir
self.thread1 = Thread(target = self.recon.matcher)
self.thread1.daemon = True
self.thread1.start()
def start_mesher(self):
"""
Handler method for starting the mesher (:ref:`pymeshlab <meshlab>`), called via "3: Mesher"
button push in :ref:`PipelineGUI <pipelineGUI>`.
It starts a new thread for the `mesher` method in the :ref:`ReconManager <reconmanager>`
class.
"""
self.recon.imgdir = self.imgdir
self.thread1 = Thread(target = self.recon.mesher)
self.thread1.daemon = True
self.thread1.start()
def auto_recon(self):
"""
Handler method for the automatic reconstuction feature, called by a command in the
`Reconstruction` file menu in :ref:`AppWindow <appwindow>`.
It starts a new thread for the `auto` method in the :ref:`ReconManager <reconmanager>`
class.
.. warning::
Using :ref:`ReconManager <reconmanager>`'s `auto` method does not allow the user to
trim the point cloud. It's useful when running the app on a large dataset or overnight,
but will likely result in a final mesh that includes outlier points.
"""
if not self.imgdir:
self.page2.log("No images loaded")
return
self.recon.imgdir = self.imgdir
self.thread1 = Thread(target = self.recon.auto)
self.thread1.daemon = True
self.thread1.start()
def cancel_recon(self):
"""
Handler method for cancelling the reconstruction, no matter the step it's on. It's called
by the "Cancel" button :ref:`PipelineGUI <pipelineGUI>`, and when changing project
directory with :ref:`PipelineGUI <pipelineGUI>`'s `change_projdir`.
"""
self.recon.cancel()
def preview_cloud(self):
"""
Handler method for the "Preview Point Cloud" button beneath the chart in
:ref:`PipelineGUI <pipelineGUI>`. It starts a subprocess to open an ``open3d`` viewer
window of the current project file, which will remain open even if the **VirtualRocks**
window is closed.
"""
path = Path(self.projdir / 'dense' / 'fused.ply')
p = subprocess.Popen([PYTHONPATH, 'scripts/CloudPreviewer.py', str(path)])
def update_state(self, state):
"""
Method for updating the state of the application, which controls which buttons are
activated and the image the chart is set to. The method also controls the value of the
total progress bar. Called when opening projects and progressing through the pipeline to
gradually enable functionality when applicable.
Args:
state (int): flag to track the state of the application.
"""
self.state = state
self.page2.progresstotal.config(value=state)
# Get correct chart
if self.page2.viewtype:
chart = "height_map.png"
else:
chart = "heat_map.png"
if state == STARTED:
self.page2.set_chart(self.page2.DEFAULT_CHART)
self.page2.matcher.config(state='disabled')
self.page2.trimbounds.config(state='disabled')
self.page2.resetbounds.config(state='disabled')
self.page2.mesher.config(state='disabled')
self.page2.previewcloud.config(state='disabled')
self.page2.chartview.config(state='disabled')
self.page2.show.config(state='disabled')
if state == PHOTOS:
self.page2.set_chart(self.page2.DEFAULT_CHART)
self.page2.trimbounds.config(state='disabled')
self.page2.resetbounds.config(state='disabled')
self.page2.mesher.config(state='disabled')
self.page2.previewcloud.config(state='disabled')
self.page2.chartview.config(state='disabled')
self.page2.show.config(state='disabled')
self.page2.matcher.config(state='active')
if state == MATCHER:
dense = Path(self.projdir / "dense")
self.page2.set_chart(Path(dense/ chart))
self.page2.show.config(state='disabled')
self.page2.matcher.config(state='active')
self.page2.previewcloud.config(state='active')
self.page2.chartview.config(state='active')
self.page2.resetbounds.config(state='active')
self.page2.trimbounds.config(state='active')
self.page2.mesher.config(state='active')
if state == MESHER:
dense = Path(self.projdir / "dense")
self.page2.set_chart(Path(dense/ chart))
self.page2.matcher.config(state='active')
self.page2.chartview.config(state='active')
self.page2.resetbounds.config(state='active')
self.page2.trimbounds.config(state='active')
self.page2.resetbounds.config(state='active')
self.page2.mesher.config(state='active')
self.page2.show.config(state='active')
try:
path = self.imgdir.relative_to(self.projdir)
except:
path = self.imgdir
with open(self.picklepath, 'wb') as file:
pickle.dump((path,state), file)
def _toggle_fullscreen(self, event=None):
"""
Event handler bound to <F11> key.
Args:
e(event): for tkinter event handlers.
"""
if self.fullscreen:
self.attributes('-fullscreen', False)
self.fullscreen = False
return 'break'
if not self.fullscreen:
self.attributes('-fullscreen', True)
self.fullscreen = True
return 'break'
def _end_fullscreen(self, event=None):
"""
Event handler bound to <Esc> key.
Args:
e (event): for tkinter event handlers.
"""
if self.fullscreen:
self.attributes('-fullscreen',False)
self.fullscreen = False
def _open_middle(self, windoww, windowh):
"""
Helper function to set the size and position of the app window on open.
Args:
windoww (int): width of the window.
windowh (int): height of the window.
"""
sw = self.winfo_screenwidth()
sh = self.winfo_screenheight()
midx = (sw/2) - (windoww/2)
midy = (sh/2) - (windowh/2)
return (midx, midy-50)
def _get_progress(self):
"""
Helper function to get the progress of the current substep. Needed to
preserve progress when switching styles.
Returns:
int: percentage of progress made on the current step.
"""
if self.recon:
return self.recon.progresspercent
else:
return -1
def _shutdown(self):
"""
Handler for <WM_DELETE_WINDOW> event. Method will be called whenever the
user closes the tkinter app. Cancels any existing reconstruction processes.
"""
try:
self.recon.cancel()
except:
print("no active processes")
print("exiting app")
self.destroy()
if __name__ == "__main__":
pklfile = None
try:
pklfile = sys.argv[1]
print(pklfile)
except:
pass
app = main(pklfile)
app.protocol("WM_DELETE_WINDOW", app._shutdown)
app.mainloop()