Source code for imzML_Writer.imzML_Writer

import tkinter as tk
from tkinter import ttk, filedialog,messagebox
import os
import threading
import sys
import time
from importlib import resources

import imzml_writer.imzML_Scout as scout
from imzml_writer.utils import *
from imzml_writer import __version__

timing_mode = False
PC_compiled = False
_on_startup=True
tries = 0

##Colors and FONTS
TEAL = "#2da7ad"
BEIGE = "#dbc076"
GREEN = "#22d10f"
FONT = ("HELVETICA", 18, 'bold')

[docs] def gui(tgt_dir:str=None): global _on_startup """Main control loop for imzML_Writer GUI. No arguments required, but if a directory is passed imzML writer will launch with that directory opened. :param tgt_dir: (optional) - initial directory for imzML Writer to open in (str)""" ##UI Functions def get_path(): """No arguments, prompts the user via dialog box for the directory containing the data to be processed. Will call populate_list() method to show files in the UI listbox""" global FILE_TYPE directory = filedialog.askdirectory(initialdir=os.getcwd()) if directory: CD_entry.delete(0,tk.END) CD_entry.insert(0,directory) populate_list(directory) FILE_TYPE = get_file_types(directory) if FILE_TYPE.lower() != "mzML".lower(): mzML_process.grid_remove() imzML_metadata.grid_remove() elif FILE_TYPE.lower() == "mzML".lower(): full_process.grid_remove() imzML_metadata.grid_remove() elif FILE_TYPE.lower() == "imzML".lower(): full_process.grid_remove() mzML_process.grid_remove() def populate_list(dir:str): """takes an argument dir and populates the UI listbox based on its contents dir: pathname for active directory as a string""" file_list.delete(0,tk.END) files = os.listdir(dir) human_sort(files) # files.sort() ticker = 0 for file in files: if not file.startswith(".") and not file.endswith(".ibd"): if file.endswith(".imzML"): search_txt = file.split(".imzML")[0] + ".ibd" if search_txt in files: file_list.insert(ticker,file) ticker+=1 else: file_list.insert(ticker,file) ticker+=1 def get_file_types(dir) -> str: """dir: pathname for active directory returns file_type as a str [taken as first non-hidden (i.e. doesn't start with ".") file in the directory]""" files = os.listdir(dir) for file in files: split_file = file.split(".") file_type = split_file[-1] file_type_label = tk.Label(text=f"File type: .{file_type}",bg=TEAL,font=FONT) file_type_label.grid(row=1,column=3,columnspan=3) return file_type def full_convert(): """Initiates file conversion from vendor format in the current directory""" if timing_mode: global tic tic = time.time() #RAW to mzML conversion, then call mzML to imzML function file_type = get_file_type(CD_entry.get()) msconvert_call = threading.Thread(target=RAW_to_mzML,kwargs={"path":CD_entry.get(),"write_mode":write_option_var.get(), "combine_ion_mobility": combine_ion_mobility.get()}) msconvert_call.start() RAW_progress.config(mode="indeterminate") RAW_progress.start() follow_raw_progress(file_type,msconvert_call) def follow_raw_progress(raw_filetype:str,convert_thread:threading.Thread): """Monitors progress of raw file conversion to mzML by comparing the number of raw vendor files to mzML files in the working directory Input: raw_filetype: string specifying the file extension of the raw files""" global tic still_active = convert_thread.is_alive() #Retrieve list of files in working directory files = os.listdir(CD_entry.get()) num_raw_files = 0 num_mzML_files = 0 mzML_files = [] ##Iterate through each file, counting each type for file in files: if file.startswith(".")==False: if f".{raw_filetype}" in file: num_raw_files+=1 elif "mzML" in file: num_mzML_files+=1 mzML_files.append(file) #Calculate progress based on number of each progress = int(num_mzML_files * 100 / num_raw_files) #Update progress bar to show how many mzML files are finished compared to total if progress > 0: RAW_progress.stop() RAW_progress.config(mode="determinate",value=progress) #If not finished, start this function over again after waiting 3 seconds for more progress to be made if progress < 100 or still_active: window.after(3000,lambda:follow_raw_progress(raw_filetype, convert_thread)) #If finished, move on to the next stage in the process elif progress >= 100 and not still_active: if timing_mode: global tic toc = time.time() print(f"RAW to mzML: {round(toc - tic,1)}s") #Clean up file structure by placing mzML and raw files in separate folders clean_raw_files(path=CD_entry.get(),file_type=raw_filetype) #Make it obvious the process is complete by changing the label to green RAW_label.config(fg=GREEN) ##Change the directory to the new mzML folder new_path = os.path.join(CD_entry.get(),"Output mzML Files") CD_entry.delete(0,tk.END) CD_entry.insert(0,new_path) populate_list(CD_entry.get()) ##Initiate the next step in the pipeline window.after(500,mzML_to_imzML()) def mzML_to_imzML(): """Run main conversion script from mzML to imzML, stop at annotation stage""" cur_path = CD_entry.get() ##Retrieve settings duplicate_spectra = duplicate_bool.get() zero_indexed = index_bool.get() search_tolerance = search_tol.get() try: search_tolerance = float(search_tolerance) except: search_tolerance = 20 messagebox.showwarning(title="ERROR",message="Invalid search tolerance specified - proceeding with 20 ppm") ##Start the progress bar whirling to indicate to user that things are working write_imzML_progress.config(mode="indeterminate") write_imzML_progress.start() if os.path.basename(cur_path) != "Output mzML Files": clean_raw_files(cur_path," ") cur_path = os.path.join(cur_path,"Output mzML Files") ##Start thread to convert the process thread = threading.Thread( target=lambda:mzML_to_imzML_convert( PATH=cur_path, progress_target=write_imzML_progress, LOCK_MASS=lock_mass_entry.get(), TOLERANCE=search_tolerance, no_duplicating=duplicate_spectra, zero_indexed=zero_indexed)) thread.daemon=True thread.start() ##Start monitoring process to see if imzML files have been successfully written check_imzML_completion(thread) def check_imzML_completion(thread): """monitors imzML conversion process by checking if the thread is still alive""" if thread.is_alive(): window.after(2000,check_imzML_completion,thread) #If thread is still going, check back again in 2 seconds else: #Otherwise, move on to the next step if timing_mode: global tic toc = time.time() print(f"mzML to imzML: {round(toc - tic,1)}s") #Update progress bar label to green to make it obvious things have completed write_imzML_Label.config(fg=GREEN) ##Update folder to directory where the intermediate imzML files are saved (directory w/ the python code) full_path = CD_entry.get() new_path = os.path.dirname(full_path) CD_entry.delete(0,tk.END) CD_entry.insert(0,new_path) populate_list(os.getcwd()) ##Initiate the metadata writing process write_metadata(path_in="indirect") def write_metadata(path_in:str="direct"): """Initiates metadata writing on the intermediate mzML files""" global path_to_models #Start progress bar whirring to indicate process has started to user Annotate_progress.config(mode="indeterminate") Annotate_progress.start() #If conversion was called directly, prompt user for source mzml files to retrieve metadata from if path_in == "direct": path_to_models = filedialog.askdirectory(initialdir=os.getcwd()) else: path_to_models = os.path.join(CD_entry.get(),"Output mzML Files") #Start the annotation in a new thread thread = threading.Thread( target=lambda:imzML_metadata_process( model_files=path_to_models, x_speed=int(speed_entry.get()), y_step=int(Y_step_entry.get()), tgt_progress=Annotate_progress, path=CD_entry.get())) thread.daemon=True thread.start() ##Monitor the annotation check_metadata_completion(thread) def check_metadata_completion(thread): """Follows metadata writing process, moving on if thread has terminated or checking again if it hasn't""" global path_to_models ##If thread is still going, run this function again after waiting 2 seconds if thread.is_alive(): window.after(2000,check_metadata_completion,thread) else: ##Otherwise, move on if timing_mode: global tic toc = time.time() print(f"imzML metadata: {round(toc - tic,1)}s") ##Update label to green, update file list to indicate process completion to user Annotate_recalibrate_label.config(fg=GREEN) model_file_list = os.listdir(path_to_models) model_file_list.sort() str_array = [letter for letter in model_file_list[0]] OUTPUT_NAME = "".join(str_array) while OUTPUT_NAME not in model_file_list[-1]: str_array.pop(-1) OUTPUT_NAME = "".join(str_array) new_path = f"{CD_entry.get()}/{OUTPUT_NAME}" CD_entry.delete(0,tk.END) CD_entry.insert(0,new_path) populate_list(CD_entry.get()) def launch_scout(): """Launches imzML_Scout.py when the user selects an imzML file on the indicated file""" if PC_compiled: messagebox.showwarning(title="imzML Scout Unavailable...",message="On PC, imzML Scout is only available using the python distributable from pypi. See the Github page for installation instructions.") else: tgt_file = file_list.selection_get() if tgt_file.split(".")[-1]=="ibd": file_start = tgt_file.split("ibd")[0] tgt_file = file_start+"imzML" path = CD_entry.get() file_path = f"{path}/{tgt_file}" scout.main(tgt_file=file_path) def resource_path(relative_path): """Future placeholder for making standalone application work""" try: base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) def launch_advanced(): def update_search_tol(*args): try: float(lock_mass_search_tol_entry.get()) search_tol.set(lock_mass_search_tol_entry.get()) except: messagebox.showwarning(title="ERROR",message="Search tolerance must be specified as number") def update_index_bool(*args): if not index_bool.get(): index_bool.set(True) else: index_bool.set(False) def update_duplicate_bool(*args): if not duplicate_bool.get(): duplicate_bool.set(True) else: duplicate_bool.set(False) def update_mobility_bool(*args): if not combine_ion_mobility.get(): combine_ion_mobility.set(True) else: combine_ion_mobility.set(False) advanced_window = tk.Tk() advanced_window.title("Advanced Options...") advanced_window.config(padx=5,pady=5,bg=TEAL) #0 vs 1 indexed (checkbox, 1 default, 0 optional) index_check = tk.Checkbutton(advanced_window,text="0-Indexed?",bg=TEAL,font=FONT,variable=index_bool,command=update_index_bool) index_check.grid(row=1,column=1,columnspan=2) if index_bool.get(): index_check.select() #Duplicate pixels? (checkbox, duplicate default) duplicate_check = tk.Checkbutton(advanced_window,text="no duplicated spectra?",bg=TEAL,font=FONT,command=update_duplicate_bool) duplicate_check.grid(row=2,column=1,columnspan=2) if duplicate_bool.get(): duplicate_check.select() #Combine ion mobility spectra combine_ion_mob_entry = tk.Checkbutton(advanced_window,text="Combine ion mobility spectra?", bg=TEAL, font=FONT,command=update_mobility_bool) combine_ion_mob_entry.grid(row=3,column=1,columnspan=2) if combine_ion_mobility.get(): combine_ion_mob_entry.select() #Lock mass search tolerance (entry, 20 ppm default) lock_mass_search_tol_label = tk.Label(advanced_window,text="Lock mass search tolerance (ppm):",bg=TEAL,font=FONT) lock_mass_search_tol_entry = tk.Entry(advanced_window,text="Enter tolerance here",highlightbackground=TEAL,background=BEIGE,fg="black",justify='center') lock_mass_search_tol_entry.insert(0,search_tol.get()) lock_mass_search_tol_entry.bind("<Return>",update_search_tol) lock_mass_search_tol_entry.bind("<FocusOut>",update_search_tol) lock_mass_search_tol_entry.grid(row=4,column=2) lock_mass_search_tol_label.grid(row=4,column=1) def bring_to_front(): window.lift() window.attributes("-topmost",True) window.after(100, lambda: window.attributes('-topmost',False)) window.focus_force() ##Build tkinter window window = tk.Tk() window.title(f"imzML Writer v{__version__}") window.config(padx=5,pady=5,bg=TEAL) style = ttk.Style() style.theme_use('clam') ##Logo try: canvas = tk.Canvas(width = 313,height=205,bg=TEAL,highlightthickness=0) img = tk.PhotoImage(file=resource_path("Images/Logo-01.png")) window.iconbitmap(resource_path("Images/imzML_Writer.ico")) canvas.create_image(313/2, 205/2,image=img) canvas.grid(column=0,row=0,columnspan=2) except: try: canvas = tk.Canvas(width = 313,height=205,bg=TEAL,highlightthickness=0) with resources.path('imzml_writer.Images','Logo-01.png') as path: img=tk.PhotoImage(file=resource_path(path)) canvas.create_image(313/2, 205/2,image=img) canvas.grid(column=0,row=0,columnspan=2) window.iconphoto(True, img) except Exception as e: print(e) ##Initialize defaults for advanced options search_tol = tk.StringVar(window) search_tol.set("20") duplicate_bool = tk.BooleanVar(window) duplicate_bool.set(False) index_bool = tk.BooleanVar(window) index_bool.set(False) combine_ion_mobility = tk.BooleanVar(window) combine_ion_mobility.set(False) ##Scan-speed entry speed_label=tk.Label(text="x scan speed (µm/s):",bg=TEAL,font=FONT) speed_entry = tk.Entry(text="Enter speed here",highlightbackground=TEAL,background=BEIGE,fg="black",justify='center') speed_entry.insert(0,"40") speed_label.grid(row=2,column=0) speed_entry.grid(row=2,column=1) ##Y-step entry Y_step_label=tk.Label(text="y step (µm):",bg=TEAL,font=FONT) Y_step_entry=tk.Entry(highlightbackground=TEAL,background=BEIGE,fg="black",justify='center') Y_step_entry.insert(0,"150") Y_step_label.grid(row=3,column=0) Y_step_entry.grid(row=3,column=1) ##Lock mass entry lock_mass_label=tk.Label(text="Lock Mass:",bg=TEAL,font=FONT) lock_mass_entry=tk.Entry(highlightbackground=TEAL,background=BEIGE,fg="black",justify='center') lock_mass_entry.insert(0,"0") lock_mass_label.grid(row=4,column=0) lock_mass_entry.grid(row=4,column=1) ##Choose Directory Button CD_button = tk.Button(text="Select Folder",bg=TEAL,highlightbackground=TEAL,command=get_path) CD_button.grid(row=1,column=0) CD_entry = tk.Entry(text="Enter Directory Here",highlightbackground=TEAL,background=BEIGE,fg="black",justify='center') CD_entry.grid(row=1,column=1) ##RAW conversion progress bar RAW_label = tk.Label(text="RAW --> mzML:",bg=TEAL,font=FONT) RAW_label.grid(row = 5,column=0) RAW_progress = ttk.Progressbar(length=525,style="danger.Striped.Horizontal.TProgressbar") RAW_progress.grid(row=5,column=1,columnspan=5) ##Write imzML progress bar write_imzML_Label=tk.Label(text="Write imzML:",bg=TEAL,font=FONT) write_imzML_Label.grid(row=6,column=0) write_imzML_progress=ttk.Progressbar(length=525,style="info.Striped.Horizontal.TProgressbar") write_imzML_progress.grid(row=6,column=1,columnspan=5) ##Annotate / m/z recalibration progress bar: Annotate_recalibrate_label = tk.Label(text="Metadata:",bg=TEAL,font=FONT) Annotate_recalibrate_label.grid(row=7,column=0) Annotate_progress=ttk.Progressbar(length=525,style="success.Striped.Horizontal.TProgressbar") Annotate_progress.grid(row=7,column=1,columnspan=5) #Listbox for files in target folder file_list = tk.Listbox(window,bg=BEIGE,fg="black",height=10,highlightcolor=TEAL,width=35,justify='left') file_list.grid(row=0,column=4,rowspan=2,columnspan=3) ##Processing buttons full_process = tk.Button(text="Full Conversion",bg=TEAL,highlightbackground=TEAL,command=full_convert) full_process.grid(row=2,column=4) mzML_process = tk.Button(text="mzML to imzML",bg=TEAL,highlightbackground=TEAL,command=mzML_to_imzML) mzML_process.grid(row=2,column=5) imzML_metadata = tk.Button(text="Write imzML metadata",bg=TEAL,highlightbackground=TEAL,command=write_metadata) imzML_metadata.grid(row=3,column=4) #Advanced option adv_options = tk.Button(text="Advanced Options...",bg=TEAL,highlightbackground=TEAL,command=launch_advanced) adv_options.grid(row=3,column=5) ##Visualize .imzML visualize = tk.Button(text="View imzML",bg=TEAL,highlightbackground=TEAL,command=launch_scout) visualize.grid(row=4,column=5,columnspan=2) ##Centroid or Profile? data_writing_options = ["Centroid", "Profile"] write_option_var = tk.StringVar(window) write_option_var.set(data_writing_options[0]) write_options_dropdown=tk.OptionMenu(window,write_option_var,*data_writing_options) write_options_dropdown.grid(row=4,column=4) if _on_startup: bring_to_front() _on_startup = False if tgt_dir != None: CD_entry.delete(0,tk.END) CD_entry.insert(0,tgt_dir) populate_list(tgt_dir) FILE_TYPE = get_file_types(tgt_dir) if FILE_TYPE.lower() != "mzML".lower(): mzML_process.grid_remove() imzML_metadata.grid_remove() elif FILE_TYPE.lower() == "mzML".lower(): full_process.grid_remove() imzML_metadata.grid_remove() elif FILE_TYPE.lower() == "imzML".lower(): full_process.grid_remove() mzML_process.grid_remove() window.mainloop()
if __name__=="__main__": try: gui(sys.argv[1]) except: gui()