Skip to content

Commit 0dec634

Browse files
committed
move gui to thread over subprocess
1 parent cf5bc2a commit 0dec634

9 files changed

Lines changed: 147 additions & 98 deletions

File tree

correct_images_gui_onefile.spec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ block_cipher = None
66
a = Analysis(['scripts\\correct_images_gui.py'],
77
pathex=['.'],
88
binaries=[],
9-
datas=[('exiftool/exiftool.exe', '.'), ('cfg/exiftool.cfg', 'cfg'), ('cfg/reg_config.ini', 'cfg'), ('scripts/correct_images.py', '.')],
9+
datas=[('exiftool/exiftool.exe', '.'), ('cfg/exiftool.cfg', 'cfg'), ('cfg/reg_config.ini', 'cfg'), ('sentera_radiometric_corrections_icon.ico', '.')],
1010
hiddenimports=['pkg_resources.py2_warn'],
1111
hookspath=[],
1212
runtime_hooks=[],
13-
excludes=['FixTk', 'tcl', 'pyinstaller'],
13+
excludes=[],
1414
win_no_prefer_redirects=False,
1515
win_private_assemblies=False,
1616
cipher=block_cipher,
@@ -31,4 +31,4 @@ exe = EXE(pyz,
3131
upx=False,
3232
upx_exclude=[],
3333
runtime_tmpdir=None,
34-
console=True )
34+
console=False )

imgcorrect/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
correct_images,
1111
)
1212
from imgcorrect.io import (
13+
TqdmToLogger,
1314
apply_sensor_settings,
1415
create_cal_df,
1516
create_image_df,
@@ -34,4 +35,5 @@
3435
"move_corrected_images",
3536
"write_image",
3637
"copy_exif",
38+
"TqdmToLogger",
3739
]

imgcorrect/corrections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def get_corrections(
223223
"""
224224
# Create new `pandas` methods which use `tqdm` progress
225225
# (can use tqdm_gui, optional kwargs, etc.)
226-
tqdm.pandas()
226+
tqdm.pandas(unit="image", file=io.TqdmToLogger(logger))
227227

228228
logger.info("ILS corrections: %s", "Disabled" if no_ils_correct else "Enabled")
229229

imgcorrect/io.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,22 @@ def write_corrections_csv(image_df, cal_df, file):
201201
(lambda x: os.path.relpath(x, base_dir))
202202
)
203203
csv_df.to_csv(file, index=False)
204+
205+
206+
class TqdmToLogger:
207+
"""Redirect tqdm output to a logger instance."""
208+
209+
def __init__(self, logger, level=logging.INFO):
210+
"""Initialize instance."""
211+
self.logger = logger
212+
self.level = level
213+
214+
def write(self, msg):
215+
"""Write message to logger."""
216+
msg = msg.strip()
217+
if msg:
218+
self.logger.log(self.level, msg)
219+
220+
def flush(self):
221+
"""No-op for flush method."""
222+
pass

imgcorrect/metadata.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def copy_exif(image_df_row, exiftool_path):
3939
]
4040
command.append(image_df_row.temp_path)
4141

42-
results = subprocess.run(command, capture_output=True)
42+
results = subprocess.run(
43+
command, capture_output=True, creationflags=subprocess.CREATE_NO_WINDOW
44+
)
4345
if results.returncode != 0:
4446
raise ValueError("Exiftool command did not run successfully.")

imgcorrect/thermal_convert.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import numpy as np
1111
from tqdm import tqdm
1212

13+
from imgcorrect.io import TqdmToLogger
14+
1315
## 12-bit support requires pip install imagecodecs
1416

1517
logger = logging.getLogger(__name__)
@@ -29,7 +31,7 @@ def convert_thermal(input_path, output_path, exiftool_path):
2931
logger.info("Converting LWIR images from CentiKelvin(uint16) to Celsius(float32)")
3032
with tempfile.TemporaryDirectory() as temp_dir:
3133
# copy image to output location
32-
for image in tqdm(images):
34+
for image in tqdm(images, unit="image", file=TqdmToLogger(logger)):
3335

3436
if "CAL" not in image:
3537
# Copy image to temporary directory
@@ -62,7 +64,11 @@ def convert_thermal(input_path, output_path, exiftool_path):
6264
"-all",
6365
output_image_path,
6466
]
65-
results = subprocess.run(command, capture_output=True)
67+
results = subprocess.run(
68+
command,
69+
capture_output=True,
70+
creationflags=subprocess.CREATE_NO_WINDOW,
71+
)
6672
if results.returncode != 0:
6773
raise ValueError("Exiftool command did not run successfully.")
6874

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "imgcorrect"
3-
version = "2.0.0"
3+
version = "2.0.1"
44
description = "Library to perform various corrections on imagery from supported sensors"
55
authors = ["Samuel Williams <sam.williams@sentera.com>", "Joseph Franck <joseph.franck@sentera.com>"]
66

run_gui.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python scripts/correct_images_gui.py

scripts/correct_images_gui.py

Lines changed: 109 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,72 @@
1+
import logging
12
import os
2-
import subprocess
33
import sys
4+
import threading
45
import tkinter as tk
56
from tkinter import filedialog, messagebox
67

7-
SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "correct_images.py")
8+
from imgcorrect import corrections
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class TextHandler(logging.Handler):
14+
def __init__(self, text_widget):
15+
super().__init__()
16+
self.text_widget = text_widget
17+
18+
def emit(self, record):
19+
msg = self.format(record)
20+
# Get the index of the last line
21+
last_line_index = self.text_widget.index("end-2l")
22+
last_line_text = self.text_widget.get(last_line_index, "end-1c").strip()
23+
if last_line_text and last_line_text[0].isdigit():
24+
# Replace the last line
25+
self.text_widget.after(
26+
0, self.text_widget.delete, last_line_index, "end-1c"
27+
)
28+
self.text_widget.after(0, self.text_widget.insert, tk.END, msg + "\n")
29+
else:
30+
self.text_widget.after(0, self.text_widget.insert, tk.END, msg + "\n")
31+
self.text_widget.after(0, self.text_widget.see, tk.END)
832

933

1034
class CorrectImagesApp(tk.Tk):
1135
def __init__(self):
1236
super().__init__()
1337
self.title("Sentera Radiometric Corrections")
14-
self.geometry("600x500")
15-
self.iconbitmap("sentera_radiometric_corrections_icon.ico")
38+
self.geometry("800x500")
39+
try:
40+
self.iconbitmap(
41+
os.path.join(sys._MEIPASS, "sentera_radiometric_corrections_icon.ico")
42+
)
43+
except Exception:
44+
self.iconbitmap("sentera_radiometric_corrections_icon.ico")
1645
self.create_widgets()
1746

1847
def create_widgets(self):
1948
row = 0
20-
tk.Label(self, text="Input Path").grid(row=row, column=0, sticky="w", padx=15)
49+
tk.Label(self, text="Input Path").grid(
50+
row=row, column=0, sticky="w", padx=(15, 0)
51+
)
2152
self.input_path_var = tk.StringVar()
2253
tk.Entry(
23-
self, textvariable=self.input_path_var, width=50, justify="right"
24-
).grid(row=row, column=1, sticky="e", padx=(0, 5))
54+
self, textvariable=self.input_path_var, width=90, justify="right"
55+
).grid(row=row, column=1, sticky="e")
2556
self.browse_input_button = tk.Button(
2657
self, text="Browse", command=self.browse_input
2758
)
2859
self.browse_input_button.grid(row=row, column=2)
2960
row += 1
3061

31-
tk.Label(self, text="Output Path").grid(row=row, column=0, sticky="w", padx=15)
62+
tk.Label(self, text="Output Path").grid(
63+
row=row, column=0, sticky="w", padx=(15, 0)
64+
)
3265
self.output_path_var = tk.StringVar()
33-
tk.Entry(
34-
self, textvariable=self.output_path_var, width=50, justify="right"
35-
).grid(row=row, column=1, sticky="e", padx=(0, 5))
66+
self.output_path_text = tk.Entry(
67+
self, textvariable=self.output_path_var, width=90, justify="right"
68+
)
69+
self.output_path_text.grid(row=row, column=1, sticky="e")
3670
self.browse_output_button = tk.Button(
3771
self, text="Browse", command=self.browse_output
3872
)
@@ -62,11 +96,11 @@ def create_widgets(self):
6296

6397
self.exiftool_path_var = tk.StringVar()
6498
self.exiftool_path_label = tk.Label(self, text="ExifTool Path (optional)")
65-
self.exiftool_path_label.grid(row=row, column=0, sticky="w", padx=15)
99+
self.exiftool_path_label.grid(row=row, column=0, sticky="w", padx=(15, 0))
66100
self.exiftool_entry = tk.Entry(
67-
self, textvariable=self.exiftool_path_var, width=50, justify="right"
101+
self, textvariable=self.exiftool_path_var, width=90, justify="right"
68102
)
69-
self.exiftool_entry.grid(row=row, column=1, sticky="w", padx=(0, 5))
103+
self.exiftool_entry.grid(row=row, column=1, sticky="e")
70104
self.exiftool_path_browse_button = tk.Button(
71105
self, text="Browse", command=self.browse_exiftool
72106
)
@@ -92,8 +126,9 @@ def create_widgets(self):
92126
self.delete_original_var = tk.BooleanVar()
93127
self.delete_overwrite_checkbutton = tk.Checkbutton(
94128
self,
95-
text="Delete/Overwrite Original Images",
129+
text="Delete/Overwrite Original",
96130
variable=self.delete_original_var,
131+
command=self.toggle_overwrite,
97132
)
98133
self.delete_overwrite_checkbutton.grid(row=row, column=0, sticky="w", padx=15)
99134
row += 1
@@ -144,12 +179,24 @@ def toggle_advanced_options(self):
144179
for widget in widgets:
145180
widget.grid()
146181

182+
def toggle_overwrite(self):
183+
if self.delete_original_var.get():
184+
self.output_path_var.set(self.input_path_var.get())
185+
self.browse_output_button["state"] = "disabled"
186+
self.output_path_text["state"] = "disabled"
187+
else:
188+
self.browse_output_button["state"] = "normal"
189+
self.output_path_text["state"] = "normal"
190+
147191
def browse_input(self):
148192
path = filedialog.askdirectory()
149193
if path:
150194
self.input_path_var.set(path)
151195
# Set output path to input path + '-calibrated'
152-
calibrated_path = path.rstrip("/\\") + "-calibrated"
196+
if not self.delete_original_var.get():
197+
calibrated_path = path.rstrip("/\\") + "-calibrated"
198+
else:
199+
calibrated_path = path
153200
self.output_path_var.set(calibrated_path)
154201
# Scroll input Entry to the end
155202
entry_widget = self.nametowidget(self.children["!entry"])
@@ -187,94 +234,66 @@ def enable_buttons(self):
187234
self.exiftool_path_browse_button["state"] = "normal"
188235

189236
def run_correction(self):
190-
import threading
191237

192238
self.disable_buttons()
193239
input_path = self.input_path_var.get()
194240
if not input_path:
195241
messagebox.showerror("Error", "Input path is required.")
242+
self.enable_buttons()
196243
return
197-
cmd = ["python", "-u", SCRIPT_PATH, input_path]
198-
if self.calibration_id_var.get():
199-
cmd += ["--calibration_id", self.calibration_id_var.get()]
200-
if self.output_path_var.get():
201-
cmd += ["--output_path", self.output_path_var.get()]
202-
if not self.ils_var.get():
203-
cmd.append("--no_ils_correct")
204-
if not self.reflectance_var.get():
205-
cmd.append("--no_reflectance_correct")
206-
if self.all_panels_var.get():
207-
cmd.append("--all_panels")
208-
if self.delete_original_var.get():
209-
cmd.append("--delete_original")
244+
calibration_id = self.calibration_id_var.get()
245+
output_path = self.output_path_var.get()
246+
no_ils_correct = not self.ils_var.get()
247+
no_reflectance_correct = not self.reflectance_var.get()
248+
all_panels = self.all_panels_var.get()
249+
delete_original = self.delete_original_var.get()
250+
210251
if self.exiftool_path_var.get():
211-
cmd += ["--exiftool_path", self.exiftool_path_var.get()]
252+
exiftool_path = self.exiftool_path_var.get()
212253
else:
213-
cmd += ["--exiftool_path", os.path.join(sys._MEIPASS, "exiftool.exe")]
214-
if self.uint16_var.get():
215-
cmd.append("--uint16_output")
254+
exiftool_path = os.path.join(sys._MEIPASS, "exiftool.exe")
255+
uint16_output = self.uint16_var.get()
256+
216257
self.output_text.delete(1.0, tk.END)
217-
self.output_text.insert(tk.END, f"Running: {' '.join(cmd)}\n")
218258

219259
os.makedirs(self.output_path_var.get(), exist_ok=True)
220260

221-
def run_subprocess():
222-
try:
223-
# Open the output file for writing
224-
with open(
225-
os.path.join(
226-
os.path.split(self.output_path_var.get())[0],
227-
f"{os.path.split(self.output_path_var.get())[1]}_corrections_log.txt",
228-
),
229-
"w",
230-
encoding="utf-8",
231-
) as outfile:
232-
process = subprocess.Popen(
233-
cmd,
234-
stdout=subprocess.PIPE,
235-
stderr=subprocess.PIPE,
236-
text=True,
237-
bufsize=1,
238-
)
239-
240-
def read_stdout(stream):
241-
for line in iter(stream.readline, ""):
242-
if line:
243-
self.output_text.after(
244-
0, self.output_text.insert, tk.END, line
245-
)
246-
outfile.write(line)
247-
outfile.flush()
248-
stream.close()
249-
250-
def read_stderr(stream):
251-
for line in iter(stream.readline, ""):
252-
if line:
253-
self.output_text.after(
254-
0, self.output_text.delete, 1.0, tk.END
255-
)
256-
self.output_text.after(
257-
0, self.output_text.insert, tk.END, line
258-
)
259-
outfile.write(line)
260-
outfile.flush()
261-
stream.close()
262-
263-
stdout_thread = threading.Thread(
264-
target=read_stdout, args=(process.stdout,)
265-
)
266-
stderr_thread = threading.Thread(
267-
target=read_stderr, args=(process.stderr,)
268-
)
269-
stdout_thread.start()
270-
stderr_thread.start()
271-
stdout_thread.join()
272-
stderr_thread.join()
273-
except Exception as e:
274-
self.output_text.after(0, self.display_exception, e)
261+
def run_corrections():
262+
handler = TextHandler(self.output_text)
263+
logging.basicConfig(
264+
filename=os.path.join(
265+
os.path.split(output_path)[0],
266+
f"{os.path.basename(output_path)}_radiometric_corrections.log",
267+
),
268+
level=logging.INFO,
269+
format="%(asctime)s %(levelname)s:%(message)s",
270+
)
271+
logging.getLogger().addHandler(handler)
272+
logger.info("Running Corrections")
273+
logger.info(f"Input Path: {input_path}")
274+
logger.info(f"Output Path: {output_path}")
275+
logger.info(f"ExifTool Path: {exiftool_path}")
276+
logger.info(f"Reflectance Correction: {not no_reflectance_correct}")
277+
logger.info(f"ILS Correction: {not no_ils_correct}")
278+
logger.info(f"Calibration ID: {calibration_id}")
279+
logger.info(f"All Panels: {all_panels}")
280+
logger.info(f"Delete Original: {delete_original}")
281+
logger.info(f"UInt16 Output: {uint16_output}")
282+
corrections.correct_images(
283+
input_path,
284+
calibration_id,
285+
output_path,
286+
no_ils_correct,
287+
no_reflectance_correct,
288+
all_panels,
289+
delete_original,
290+
exiftool_path,
291+
uint16_output,
292+
)
275293
self.enable_buttons()
294+
logger.info("Corrections complete!")
276295

277-
threading.Thread(target=run_subprocess, daemon=True).start()
296+
threading.Thread(target=run_corrections, daemon=True).start()
278297

279298
def display_output(self, stdout, stderr):
280299
if stdout:

0 commit comments

Comments
 (0)