Skip to content

Commit 3d8a430

Browse files
authored
Merge pull request #15 from Topp-Roots-Lab/feature/direct-export-gui
Feature/direct export gui
2 parents 48dd2ea + b73d3cc commit 3d8a430

File tree

15 files changed

+456
-131
lines changed

15 files changed

+456
-131
lines changed

HISTORY.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
History
33
=======
44

5+
0.2.0 (2021-01-04)
6+
------------------
7+
8+
* Added efX-SDK library
9+
* Added module to export/convert `nsihdr` files to uint `raw` files using the efX-SDK (Windows only)
10+
* Tentatively implemented a GUI for the nsihdr2raw export tool
11+
* Tweaked logging path for log files generated relative to input data for `nsihdr2raw`
12+
13+
0.1.4 (2020-10-27)
14+
------------------
15+
16+
* Code clean up, remove unnecessary transposing of data, improved debug statements and logging
17+
18+
0.1.3 (2020-10-26)
19+
------------------
20+
21+
* Fixed font file missing when installing via pip
22+
23+
524
0.1.2 (2020-09-25)
625
------------------
726

@@ -10,4 +29,4 @@ History
1029
0.1.0 (2020-06-19)
1130
------------------
1231

13-
* First release on PyPI.
32+
* Initial version

rawtools/assets/caution.ico

4.19 KB
Binary file not shown.

rawtools/assets/caution.png

1.21 KB
Loading

rawtools/assets/tools.ico

4.19 KB
Binary file not shown.

rawtools/assets/tools.png

1.98 KB
Loading

rawtools/cli.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,12 @@
44
from importlib.metadata import version
55
from multiprocessing import cpu_count
66

7-
from rawtools import convert, generate, nsihdr, qualitycontrol, log, raw2img
7+
from rawtools import convert, generate, log, qualitycontrol, raw2img
88

99
__version__ = version('rawtools')
1010

1111
def main():
1212
"""Console script for rawtools."""
13-
parser = argparse.ArgumentParser()
14-
parser.add_argument('_', nargs='*')
15-
args = parser.parse_args()
16-
17-
print("Arguments: " + str(args._))
18-
print("Replace this message by putting your code into "
19-
"rawtools.cli.main")
2013
return 0
2114

2215
def raw_convert():
@@ -69,19 +62,27 @@ def raw_generate():
6962
generate.main(args)
7063

7164
def raw_nsihdr():
72-
description = "This tool converts a NSI project from 32-bit float to 16-bit unsigned integer format, and it extracts the midslice and generates a side-view projection of the volume."
65+
description = "This tool converts a NSI project from 32-bit float to 16-bit unsigned integer format."
7366

7467
parser = argparse.ArgumentParser(description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
7568
parser.add_argument("-V", "--version", action="version", version=f'%(prog)s {__version__}')
7669
parser.add_argument("-v", "--verbose", action="store_true", help="Increase output verbosity")
7770
parser.add_argument("-f", "--force", action="store_true", default=False, help="Force file creation. Overwrite any existing files.")
78-
parser.add_argument('path', metavar='PATH', type=str, nargs='+', help='List of .nsihdr files')
71+
parser.add_argument("--gui", action="store_true", default=False, help="(Experimental) Enable GUI")
72+
parser.add_argument('path', metavar='PATH', type=str, nargs="+", help='List of .nsihdr files')
7973
args = parser.parse_args()
8074

8175
args.module_name = 'nsihdr'
8276
log.configure(args)
8377

84-
nsihdr.main(args)
78+
# Use a GUI to select the source directory
79+
if args.gui == True:
80+
from rawtools.gui import nsihdr
81+
nsihdr.App(args)
82+
# Otherwise, assume CLI use
83+
else:
84+
from rawtools import nsihdr
85+
nsihdr.main(args)
8586

8687
def raw_qc():
8788
"""Quality control tools"""

rawtools/gui/__init__.py

Whitespace-only changes.

rawtools/gui/nsihdr.py

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
""""GUI for NSIHDR conversion tool"""
2+
3+
import logging
4+
import os
5+
import tkinter as tk
6+
from enum import Enum
7+
from importlib.metadata import version
8+
from pprint import pformat
9+
from tkinter import (E, N, S, StringVar, Toplevel, W, filedialog, ttk)
10+
11+
from rawtools import nsihdr
12+
from ttkthemes import ThemedTk
13+
14+
__version__ = version('rawtools')
15+
16+
def center(root, toplevel):
17+
toplevel.update_idletasks()
18+
19+
# Tkinter way to find the screen resolution
20+
# screen_width = toplevel.winfo_screenwidth()
21+
# screen_height = toplevel.winfo_screenheight()
22+
23+
# PyQt way to find the screen resolution
24+
screen_width = root.winfo_screenwidth()
25+
screen_height = root.winfo_screenheight()
26+
27+
size = tuple(int(_) for _ in toplevel.geometry().split('+')[0].split('x'))
28+
x = screen_width/2 - size[0]/2
29+
y = screen_height/2 - size[1]/2
30+
31+
toplevel.geometry("+%d+%d" % (x, y))
32+
33+
class App():
34+
def __init__(self, args):
35+
self.source = 'C:/Users/Tim Parker/Datasets/topp/xrt/development/batch2'
36+
self.args = args
37+
# Source: https://www.elegantthemes.com/blog/freebie-of-the-week/beautiful-flat-icons-for-free
38+
self.icon_fp = "rawtools\\assets\\tools.ico"
39+
self.icon_caution_fp = "rawtools\\assets\\caution.ico"
40+
self.state = 'idle'
41+
42+
self.root = ThemedTk(theme='arc')
43+
root = self.root
44+
root.title(f"Batch Export Tool v{__version__}")
45+
root.resizable(False, False)
46+
menubar = tk.Menu(root)
47+
48+
file_menu = tk.Menu(menubar, tearoff=False)
49+
file_menu.add_command(label="View Logs", command=lambda: print("Load logs"))
50+
file_menu.add_separator()
51+
file_menu.add_command(label="Quit", command=self.quitApplication, accelerator='Ctrl-Q')
52+
menubar.add_cascade(label="File", menu=file_menu)
53+
54+
help_menu = tk.Menu(menubar, tearoff=False)
55+
help_menu.add_command(label="About", command = None)
56+
help_menu.add_separator()
57+
help_menu.add_command(label="Documentation")
58+
menubar.add_cascade(label="Help", menu=help_menu)
59+
root.config(menu = menubar)
60+
61+
# Assign hotkey(s)
62+
root.bind("<Control-q>", self.quitApplication)
63+
64+
mainframe = ttk.Frame(root, padding="16 16")
65+
mainframe.grid(column=0, row=0, sticky=(N, S, E, W))
66+
self.mainframe = mainframe
67+
68+
root.iconbitmap(self.icon_fp)
69+
70+
# Source folder selection
71+
src_intro_label_text = "Select an NSI Reconstruction folder."
72+
src_intro_label = ttk.Label(mainframe, text=src_intro_label_text)
73+
src_intro_label.grid(row=0, column=0, sticky=(E,W), pady="0 8")
74+
75+
self.src = tk.StringVar()
76+
self.src.set(self.source)
77+
# # Add event handling to changes to the source directory text field
78+
self.src_entry = ttk.Entry(mainframe, textvariable = self.src, width=85)
79+
self.src_entry.grid(row=1, column=0, columnspan=3, sticky=(E, W), padx="0 8", pady="0 16")
80+
81+
self.src_folder_btn = ttk.Button(mainframe, text = 'Select Folder', command=self.choose_src)
82+
self.src_folder_btn.grid(row=1, column=4, columnspan=1, pady="0 16", padx="8 0")
83+
84+
# Export data
85+
self.export_btn = ttk.Button(mainframe, text = 'Export', command=self.export)
86+
self.export_btn.grid(row=2, column=0, columnspan=5, pady="0 8")
87+
88+
# Center window on screen
89+
root.update() # virtual pre-render of GUI to calculate actual sizes
90+
w = root.winfo_reqwidth()
91+
h = root.winfo_reqheight()
92+
logging.debug(f"Root width: {w}")
93+
logging.debug(f"Root height: {h}")
94+
ws = root.winfo_screenwidth()
95+
hs = root.winfo_screenheight()
96+
# calculate position x, y
97+
x = (ws/2) - (w/2)
98+
y = (hs/2) - (h/2)
99+
root.geometry('+%d+%d' % (x, y))
100+
101+
# Display window to user
102+
root.mainloop()
103+
104+
def choose_src(self):
105+
"""Select a folder to act as data source"""
106+
self.source = filedialog.askdirectory(initialdir=self.source, title="Choose directory")
107+
logging.debug(f'Selected folder: {self.source}')
108+
self.src.set(self.source)
109+
110+
def scan_folder(self, path):
111+
"""Scan folder for nsihdr and corresponding raw files
112+
113+
Args:
114+
115+
path (str): Input path
116+
"""
117+
logging.debug(f"{path=}")
118+
if len(path) < 2:
119+
return
120+
121+
# Invalid path provided, abort
122+
if not (os.path.exists(path) and os.path.isdir(path)):
123+
return
124+
125+
# Get all files
126+
files = [ files for r, d, files in os.walk(path) ][0]
127+
logging.debug(f"{files=}")
128+
129+
# Filter NSIHDR files
130+
nsihdr_files = [ f for f in files if f.endswith('.nsihdr') ]
131+
logging.debug(f"{nsihdr_files=}")
132+
133+
# Filter RAW files
134+
raw_files = [ f for f in files if f.endswith('.raw') ]
135+
logging.debug(f"{raw_files=}")
136+
137+
# Determine what RAW would be created from the NSIHDR files
138+
expected_raw_files = [ '.'.join([os.path.splitext(f)[0], 'raw']) for f in nsihdr_files ]
139+
logging.debug(f"{expected_raw_files=}")
140+
141+
142+
# # Get all files
143+
logging.debug(f"All input scans: {nsihdr_files}")
144+
nsihdr_files = list(set(nsihdr_files)) # remove duplicates
145+
logging.debug(f"Unique input scans: {nsihdr_files}")
146+
147+
return nsihdr_files, raw_files, expected_raw_files
148+
149+
def export(self):
150+
# Get selected path
151+
path = self.src.get()
152+
self.args.path = [path] # CLI requires list of paths
153+
self.cancelled = False
154+
155+
# Scan input directory for .NSIHDR files
156+
nsihdr_files, raw_files, expected_raw_files = self.scan_folder(path)
157+
158+
# Prompt user with actions
159+
# Case 1: Existing data
160+
overlapping_raw_files = list(set(raw_files) & set(expected_raw_files))
161+
logging.debug(f"{overlapping_raw_files=}")
162+
if len(overlapping_raw_files) > 0:
163+
prompt_title = "Warning - File Conflict Encountered"
164+
at_risk_files = '\n'.join(overlapping_raw_files)
165+
if len(overlapping_raw_files) == 1:
166+
prompt_message = "A conflict in the data files was encountered.\n\nThe following reconstructed volume appears to have already been exported.\n\n"+at_risk_files+"\n\nDo you want to overwrite this file? This will first *destroy* it."
167+
else:
168+
prompt_message = "A conflict in the data files was encountered.\n\nThe following reconstructed volumes appear to have already been exported.\n\n"+at_risk_files+"\n\nDo you want to overwrite these files? This will first *destroy* them."
169+
logging.warning(prompt_message)
170+
171+
self.prompt = Toplevel(self.root)
172+
self.prompt.title(prompt_title)
173+
self.prompt.iconbitmap(self.icon_caution_fp)
174+
self.prompt.resizable(False, False)
175+
self.prompt_frame = ttk.Frame(self.prompt, padding="16 16")
176+
self.prompt_frame.grid(column=0, row=0, sticky=(N, S, E, W))
177+
self.prompt_message = ttk.Label(self.prompt_frame, text=prompt_message).grid(row = 0, column = 0, columnspan=3, pady="0 32")
178+
self.prompt_button = ttk.Button(self.prompt_frame, text="Overwrite", command=self.overwrite_files).grid(row = 1, column = 0, columnspan=1)
179+
self.prompt_button = ttk.Button(self.prompt_frame, text="Skip", command=self.skip_files).grid(row = 1, column = 1, columnspan=1)
180+
self.prompt_button = ttk.Button(self.prompt_frame, text="Cancel", command=self.cancel_export).grid(row = 1, column = 2, columnspan=1)
181+
182+
# Orient window on screen
183+
center(self.root, self.prompt)
184+
# Disable interaction with parent window
185+
self.prompt.protocol("WM_DELETE_WINDOW", self.dismiss)
186+
self.prompt.transient(self.root)
187+
self.prompt.wait_visibility()
188+
self.prompt.grab_set()
189+
self.prompt.wait_window()
190+
191+
# Only new data was found
192+
else:
193+
# Case 2: New data
194+
prompt_title = "Confirm Action - Export"
195+
expected_raw_files = '\n'.join(expected_raw_files)
196+
if len(overlapping_raw_files) == 1:
197+
prompt_message = "The following file will be generated.\n\n"+expected_raw_files
198+
else:
199+
prompt_message = "The following files will be generated.\n\n"+expected_raw_files
200+
logging.debug(prompt_message)
201+
202+
self.prompt = Toplevel(self.root)
203+
self.prompt.title(prompt_title)
204+
self.prompt.iconbitmap(self.icon_fp)
205+
self.prompt.resizable(False, False)
206+
prompt_frame = ttk.Frame(self.prompt, padding="16 16")
207+
prompt_frame.grid(column=0, row=0, sticky=(N, S, E, W))
208+
self.prompt_message = ttk.Label(prompt_frame, text=prompt_message).grid(row = 0, column = 0, columnspan=4, pady="0 32")
209+
self.prompt_button = ttk.Button(prompt_frame, text="Ok", command=self.dismiss).grid(row = 1, column = 1, columnspan=1)
210+
self.prompt_button = ttk.Button(prompt_frame, text="Cancel", command=self.cancel_export).grid(row = 1, column = 2, columnspan=1)
211+
212+
# Orient window on screen
213+
center(self.root, self.prompt)
214+
# Disable interaction with parent window
215+
self.prompt.protocol("WM_DELETE_WINDOW", self.dismiss)
216+
self.prompt.transient(self.root)
217+
self.prompt.wait_visibility()
218+
self.prompt.grab_set()
219+
self.prompt.wait_window()
220+
221+
self.prompt_frame.grid_forget()
222+
self.prompt = None
223+
224+
# Process data
225+
if not self.cancelled:
226+
# Do processing
227+
logging.debug(self.args)
228+
self.args.app = self
229+
nsihdr.main(self.args)
230+
else:
231+
logging.debug(f"Cancelled export")
232+
233+
def quitApplication(self, _event=None):
234+
self.root.destroy()
235+
236+
def overwrite_files(self):
237+
# Case 1a: Overwrite all data and create new
238+
self.args.force = True
239+
self.dismiss()
240+
241+
def skip_files(self):
242+
# Case 1b: Skip all existing data
243+
self.args.force = False # just in case it was enabled via CLI
244+
self.dismiss()
245+
246+
def cancel_export(self):
247+
# Case 1b: Skip all existing data
248+
self.cancelled = True
249+
self.dismiss()
250+
251+
def dismiss(self):
252+
self.prompt.grab_release()
253+
self.prompt.destroy()
254+
255+
def dismiss_progress_prompt(self):
256+
self.progress_bar_prompt.grab_release()
257+
self.progress_bar_prompt.destroy()
258+
1.57 KB
Binary file not shown.

rawtools/lib/win32/efX-SDK.dll

1 KB
Binary file not shown.

0 commit comments

Comments
 (0)