__author__ = 'Alex Zeising' __version__ = '0.12' import os import tkinter as tk from tkinter import ttk from tkinter.filedialog import asksaveasfilename, askopenfile from tkinter.messagebox import showerror, showinfo, askyesno from threading import Thread from PIL import Image MAX_SPEED = 30 IMAGE_QUALITY = 95 # effective range stops at 100 class MainApp(object): # contains GUI (tkinter) structure and logic def __init__(self, master): self.master = master # executes self.on_closing on program exit master.protocol('WM_DELETE_WINDOW', self.on_closing) master.resizable(False, False) master.minsize(width=400, height=380) master.title('The RSE-Simulator') self.rolling_shutter = None self.files = [] self.file_output = '' self.tk_speed_val = tk.IntVar() self.tk_speed_val.set(1) self.tk_progress_val = tk.DoubleVar() self.tk_progress_val.set(0.0) # <- FRAME SECTION -> self.frame_main = tk.Frame(master) self.frame_main.pack(fill='both', expand=True) self.frame_footer = tk.Frame(master) self.frame_footer.pack(fill='both', anchor='s') # <- WIDGET SECTION -> self.label_title = ttk.Label(self.frame_main, text='Rolling-Shutter-Simulator', font=('Tahoma', 18)) self.label_title.pack(pady=(8, 20)) self.btn_input = ttk.Button(self.frame_main, text='Select Input', command=self.select_input, takefocus=0) self.btn_input.pack(fill='both', padx=40, expand=True, pady=10) self.btn_output = ttk.Button(self.frame_main, text='Select Output File', command=self.select_output, state='disabled', takefocus=0) self.btn_output.pack(fill='both', padx=40, expand=True) self.speed_scale = ttk.Scale(self.frame_main, variable=self.tk_speed_val, command=self.update_speed, from_=1, to=MAX_SPEED, length=310, takefocus=0) self.speed_scale.pack(pady=(8, 0)) self.speed_scale.state(['disabled']) self.label_speed = ttk.Label(self.frame_main, text='Shutter Speed: 1', font=('Tahoma', 13)) self.label_speed.pack(pady=(0, 8)) self.progress_bar = ttk.Progressbar(self.frame_main, orient='horizontal', mode='determinate', variable=self.tk_progress_val) self.progress_bar.pack(fill='x', padx=20, pady=(30, 0)) self.progress_bar.state(['disabled']) self.btn_start = ttk.Button(self.frame_main, text='Give it a go!', command=self.start, state='disabled', takefocus=0) self.btn_start.pack(fill='both', padx=140, pady=(8, 0), expand=True) self.label_version = tk.Label(self.frame_footer, text='Version '+__version__, font=('Tahoma', 10), fg='grey60') self.label_version.pack(anchor='e', padx=(0, 5)) def select_input(self) -> None: file = askopenfile(title='Please select one (any) frame from your set of images.', filetypes=[('Image Files', ['.jpeg', '.jpg', '.png', '.gif', '.tiff', '.tif', '.bmp'])]) if not file: return None dir_ = os.path.dirname(file.name) filetype = os.path.splitext(file.name) self.files = [os.path.abspath(os.path.join(dir_, f)) for f in os.listdir(dir_) if f.endswith(filetype)] self.files.sort() self.btn_output['state'] = 'normal' def select_output(self) -> None: path = asksaveasfilename(title='Please select the path of the image to create.', defaultextension='.png', filetypes=[('PNG File', '*.png'), ('JPEG File', '*.jpg')]) if not path: return None self.file_output = path self.speed_scale.state(['!disabled']) self.btn_start['state'] = 'normal' def start(self) -> None: rs = self.rolling_shutter = RollingShutter(self.files, self.tk_speed_val.get(), self.file_output) lines_covered = rs.frame_count * self.tk_speed_val.get() if lines_covered < rs.height: m = ('The number of iterations ({}) is lower than the height' ' of the resulting image ({}px).\n\nMissing spots ({} lines)' ' will be filled with black.\n\n' 'Do you want to continue?') message = m.format(lines_covered, rs.height, rs.height-lines_covered) choice = askyesno('Proceed?', message) if not choice: return None self.disable_buttons() self.progress_bar.config(maximum=lines_covered) self.progress_bar.state(['!disabled']) t1 = Thread(target=rs.thread, args=(self,)) t1.setDaemon(True) t1.start() def update_speed(self, event=None) -> None: self.label_speed.config(text='Shutter Speed: '+str(self.tk_speed_val.get())) def update_progress(self, value: float): self.tk_progress_val.set(value) def enable_buttons(self) -> None: self.btn_input['state'] = 'normal' self.btn_start['state'] = 'normal' self.btn_output['state'] = 'normal' self.speed_scale.state(['!disabled']) def disable_buttons(self) -> None: self.btn_input['state'] = 'disabled' self.btn_start['state'] = 'disabled' self.btn_output['state'] = 'disabled' self.speed_scale.state(['disabled']) def on_closing(self) -> None: if self.rolling_shutter and self.rolling_shutter.running: return None self.master.destroy() class RollingShutter(object): # simulates the well-known 'Rolling-Shutter-Parker-Effect' def __init__(self, frame_paths: list, speed: int, path_output: str): self.frame_paths = frame_paths self.speed = speed self.path_output = path_output self.frame_count = len(frame_paths) self.current_row = 0 width, height = Image.open(frame_paths[0]).size self.img_output = Image.new('RGB', (width, height)) self.width, self.height = width, height self.running = False def thread(self, app_obj) -> None: width, height = self.width, self.height speed = self.speed self.running = True try: for path in self.frame_paths: frame = Image.open(path) new_line = frame.crop((0, self.current_row, width, self.current_row + speed)) self.img_output.paste(new_line, (0, self.current_row)) frame.close() app_obj.update_progress(self.current_row) self.current_row += speed app_obj.update_progress(self.current_row) self.img_output.save(self.path_output, quality=IMAGE_QUALITY) app_obj.progress_bar.state(['disabled']) app_obj.enable_buttons() showinfo('Process Complete.', 'The shutter-rolled image has been created!') finally: self.running = False app_obj.update_progress(0) app_obj.progress_bar.state(['disabled']) app_obj.enable_buttons() def main() -> None: root = tk.Tk() MainApp(root) root.mainloop() if __name__ == '__main__': main()