diff --git a/qim3d/css/gradio.css b/qim3d/css/gradio.css index 8debd7a6e1b213e004bb09cd663b14ea5f925f1e..d34102b481b50ca275ac3ac504d6782e98592f67 100644 --- a/qim3d/css/gradio.css +++ b/qim3d/css/gradio.css @@ -225,8 +225,6 @@ h2 { box-shadow: none !important; border-radius: 0.375rem !important; border: 0px !important; - /* overflow-x: scroll !important; - overflow-y: scroll !important; */ white-space: pre !important; } @@ -356,6 +354,8 @@ input[type="range"]::-webkit-slider-thumb { text-decoration: none !important; font-size: 16px !important; width: 100% !important; + font-weight: normal !important; + } .btn-html:hover { @@ -551,6 +551,17 @@ div.svelte-1frtwj3 { } +.h-36 { + height: 36px !important; + +} + +.w-36 { + width: 36px !important; + min-width: 36px !important; + +} + .h-32 { height: 32px !important; @@ -599,4 +610,8 @@ div.svelte-1frtwj3 { .mt-128{ margin-top: 128px; -} \ No newline at end of file +} + +.hide-overflow{ + overflow: hidden !important; +} diff --git a/qim3d/gui/data_explorer.py b/qim3d/gui/data_explorer.py index 1d6ada8744031e230c18c4b034dc079752ecc630..bb2790f7bf1be85ab84430b53dee4700e31c54fd 100644 --- a/qim3d/gui/data_explorer.py +++ b/qim3d/gui/data_explorer.py @@ -18,10 +18,18 @@ class Interface: self.show_header = False self.verbose = False self.title = "Data Explorer" - self.height = 1024 self.width = 900 - + self.operations = [ + "Z Slicer", + "Y Slicer", + "X Slicer", + "Z max projection", + "Z min projection", + "Intensity histogram", + "Data summary", + + ] # CSS path current_dir = os.path.dirname(os.path.abspath(__file__)) self.css_path = os.path.join(current_dir, "..", "css", "gradio.css") @@ -30,120 +38,225 @@ class Interface: """Used to reset outputs with the clear button""" return None + def update_explorer(self, new_path): + new_path = os.path.expanduser(new_path) + + # In case we have a directory + if os.path.isdir(new_path): + return gr.update(root=new_path, label=new_path) + + elif os.path.isfile(new_path): + parent_dir = os.path.dirname(new_path) + file_name = str(os.path.basename(new_path)) + return gr.update(root=parent_dir, label=parent_dir, value=file_name) + + else: + raise ValueError("Invalid path") + + def set_visible(self): + return gr.update(visible=True) + + def set_spinner(self, message): + return gr.update( + elem_classes="btn btn-spinner", + value=f"{message}", + interactive=False, + ) + + def set_relaunch_button(self): + return gr.update( + elem_classes="btn btn-run", + value=f"Relaunch", + interactive=True, + ) + + def show_results(self, operations): + update_list = [] + for operation in self.operations: + if operation in operations: + update_list.append(gr.update(visible=True)) + else: + update_list.append(gr.update(visible=False)) + return update_list + def create_interface(self): with gr.Blocks(css=self.css_path) as gradio_interface: gr.Markdown("# Data Explorer") + # File selection and parameters with gr.Row(): - with gr.Column(scale=0.75): - data_path = gr.Textbox( - value="gbar/zhome/15/b/200707/img_examples/shell_225x128x128.tif", - max_lines=1, - label="Path to the 3D volume", + with gr.Column(scale=2): + gr.Markdown("### File selection") + with gr.Row(): + with gr.Column(scale=99, min_width=128): + base_path = gr.Textbox( + max_lines=1, + container=False, + label="Base path", + elem_classes="h-36", + value=os.getcwd(), + ) + with gr.Column(scale=1, min_width=36): + reload_base_path = gr.Button( + value="⟳", elem_classes="btn-html h-36" + ) + explorer = gr.FileExplorer( + glob="{*/,}{*.*}", + root=os.getcwd(), + label=os.getcwd(), + render=True, + file_count="single", + interactive=True, + elem_classes="h-256 hide-overflow", + ) + + with gr.Column(scale=1): + gr.Markdown("### Parameters") + virtual_stack = gr.Checkbox(value=False, label="Virtual stack") + + cmap = gr.Dropdown( + value="viridis", + choices=plt.colormaps(), + label="Colormap", + interactive=True, ) - with gr.Column(scale=0.25): dataset_name = gr.Textbox( - label="Dataset name (in case of H5 files, for example)" + label="Dataset name (in case of H5 files, for example)", + value="exchange/data", ) - with gr.Row(elem_classes="w-256"): - cmap = gr.Dropdown( - value="viridis", - choices=plt.colormaps(), - label="Colormap", - interactive=True, - ) - with gr.Row(elem_classes="w-128"): - btn_run = gr.Button(value="Load & Run", elem_classes="btn btn-run") - # Outputs - with gr.Row(): - gr.Markdown("## Data overview") + with gr.Column(scale=1): + gr.Markdown("### Operations") + operations = gr.CheckboxGroup( + choices=self.operations, + value=[self.operations[0], self.operations[-1]], + label=None, + container=False, + interactive=True, + ) + with gr.Row(): + btn_run = gr.Button( + value="Load & Run", elem_classes="btn btn-html btn-run" + ) - with gr.Row(): - data_summary = gr.Text( - label=None, show_label=False, elem_classes="monospace-box" - ) - with gr.Row(): - with gr.Column(): + # Visualization and results + with gr.Row(elem_classes="mt-64"): + + # Z Slicer + with gr.Column(visible=False) as result_z_slicer: zslice_plot = gr.Plot(label="Z slice", elem_classes="rounded") zpos = gr.Slider( minimum=0, maximum=1, value=0.5, step=0.01, label="Z position" ) - with gr.Column(): + + # Y Slicer + with gr.Column(visible=False) as result_y_slicer: yslice_plot = gr.Plot(label="Y slice", elem_classes="rounded") ypos = gr.Slider( minimum=0, maximum=1, value=0.5, step=0.01, label="Y position" ) - with gr.Column(): + # X Slicer + with gr.Column(visible=False) as result_x_slicer: xslice_plot = gr.Plot(label="X slice", elem_classes="rounded") xpos = gr.Slider( minimum=0, maximum=1, value=0.5, step=0.01, label="X position" ) - with gr.Row(elem_classes="h-32"): - gr.Markdown() + # Z Max projection + with gr.Column(visible=False) as result_z_max_projection: + max_projection_plot = gr.Plot( + label="Z max projection", elem_classes="rounded" + ) - with gr.Row(elem_classes="h-480"): - max_projection_plot = gr.Plot( - label="Z max projection", elem_classes="rounded" - ) - min_projection_plot = gr.Plot( - label="Z min projection", elem_classes="rounded" - ) + # Z Min projection + with gr.Column(visible=False) as result_z_min_projection: + min_projection_plot = gr.Plot( + label="Z min projection", elem_classes="rounded" + ) - hist_plot = gr.Plot(label="Volume intensity histogram") + # Intensity histogram + with gr.Column(visible=False) as result_intensity_histogram: + hist_plot = gr.Plot(label="Volume intensity histogram") + + # Text box with data summary + with gr.Column(visible=False) as result_data_summary: + data_summary = gr.Text( + lines=24, + label=None, + show_label=False, + elem_classes="monospace-box", + value="Data summary", + ) + ### Gradio objects lists - pipeline = Pipeline() - pipeline.verbose = self.verbose session = gr.State([]) + pipeline = Pipeline() - ### Gradio objects lists - + # Results + results = [ + result_z_slicer, + result_y_slicer, + result_x_slicer, + result_z_max_projection, + result_z_min_projection, + result_intensity_histogram, + result_data_summary, + ] # Inputs - inputs = [zpos, ypos, xpos, cmap, dataset_name] + inputs = [ + operations, + base_path, + explorer, + zpos, + ypos, + xpos, + cmap, + dataset_name, + virtual_stack, + ] # Outputs outputs = [ - data_summary, zslice_plot, yslice_plot, xslice_plot, max_projection_plot, min_projection_plot, - ] + hist_plot, + data_summary, - projection_outputs = [session, max_projection_plot, min_projection_plot] + ] ### Listeners - # Clear button - # for gr_obj in outputs: - # btn_clear.click(fn=self.clear, inputs=[], outputs=gr_obj) - - # Run button + spinner_session = gr.Text("Starting session...", visible=False) + spinner_loading = gr.Text("Loading data...", visible=False) + spinner_operations = gr.Text("Running pipeline...", visible=False) # fmt: off + reload_base_path.click(fn=self.update_explorer,inputs=base_path, outputs=explorer) + btn_run.click( - fn=self.start_session, inputs=inputs, outputs=session).success( - fn=pipeline.process_input, inputs=[session, data_path], outputs=session).success( - fn=pipeline.show_summary_str, inputs=session, outputs=data_summary).success( - fn=pipeline.create_zslice_fig, inputs=session, outputs=zslice_plot).success( - fn=pipeline.create_yslice_fig, inputs=session, outputs=yslice_plot).success( - fn=pipeline.create_xslice_fig, inputs=session, outputs=xslice_plot).success( - fn=pipeline.create_projections_figs, inputs=session, outputs=projection_outputs).success( - fn=pipeline.show_summary_str, inputs=session, outputs=data_summary).success( - fn=pipeline.plot_vol_histogram, inputs=session, outputs=hist_plot) + fn=self.set_spinner, inputs=spinner_session, outputs=btn_run).then( + fn=self.start_session, inputs=inputs, outputs=session).then( + fn=self.set_spinner, inputs=spinner_loading, outputs=btn_run).then( + fn=pipeline.load_data, inputs=session, outputs=session).then( + fn=self.set_spinner, inputs=spinner_operations, outputs=btn_run).then( + fn=pipeline.run_pipeline, inputs=session, outputs=outputs).then( + fn=self.show_results, inputs=operations, outputs=results).then( + fn=self.set_relaunch_button, inputs=[], outputs=btn_run) + zpos.release( fn=self.update_zpos, inputs=[session, zpos], outputs=[session, zslice_plot]).success( - fn=pipeline.create_zslice_fig, inputs=session, outputs=zslice_plot,show_progress=False) + fn=pipeline.create_zslice_fig, inputs=[], outputs=zslice_plot,show_progress="hidden") ypos.release( fn=self.update_ypos, inputs=[session, ypos], outputs=[session, yslice_plot]).success( - fn=pipeline.create_yslice_fig, inputs=session, outputs=yslice_plot,show_progress=False) + fn=pipeline.create_yslice_fig, inputs=[], outputs=yslice_plot,show_progress="hidden") xpos.release( fn=self.update_xpos, inputs=[session, xpos], outputs=[session, xslice_plot]).success( - fn=pipeline.create_xslice_fig, inputs=session, outputs=xslice_plot,show_progress=False) + fn=pipeline.create_xslice_fig, inputs=[], outputs=xslice_plot,show_progress="hidden") # fmt: on @@ -152,12 +265,24 @@ class Interface: def start_session(self, *args): # Starts a new session dictionary session = Session() - session.interface = "gradio" - session.zpos = args[0] - session.ypos = args[1] - session.xpos = args[2] - session.cmap = args[3] - session.dataset_name = args[4] + session.all_operations = Interface().operations + session.operations = args[0] + session.base_path = args[1] + session.explorer = args[2] + session.zpos = args[3] + session.ypos = args[4] + session.xpos = args[5] + session.cmap = args[6] + session.dataset_name = args[7] + session.virtual_stack = args[8] + + # Get the file path from the explorer or base path + if session.base_path and os.path.isfile(session.base_path): + session.file_path = session.base_path + elif session.explorer and os.path.isfile(session.explorer): + session.file_path = session.explorer + else: + raise ValueError("Invalid file path") return session @@ -203,8 +328,8 @@ class Interface: class Session: def __init__(self): - self.interface = None - self.data_path = None + self.virtual_stack = False + self.file_path = None self.vol = None self.zpos = 0.5 self.ypos = 0.5 @@ -212,7 +337,10 @@ class Session: self.cmap = "viridis" self.dataset_name = None self.error_message = None - + self.file_path = None + self.max_projection = None + self.min_projection = None + self.projections_calculated = False # Volume info self.zsize = None self.ysize = None @@ -231,53 +359,6 @@ class Session: # Histogram self.nbins = 32 - def get_data_info(self): - # Open file - try: - vol = load( - self.data_path, virtual_stack=True, dataset_name=self.dataset_name - ) - except Exception as error_message: - self.error_message = error_message - return - - first_slice = vol[0] - - # Get info - self.zsize = len(vol) - self.ysize, self.xsize = first_slice.shape - self.data_type = str(first_slice.dtype) - self.last_modified = datetime.datetime.fromtimestamp( - os.path.getmtime(self.data_path) - ).strftime("%Y-%m-%d %H:%M") - self.file_size = os.path.getsize(self.data_path) - - def create_summary_dict(self): - # Create dictionary - if self.error_message: - self.summary_dict = {"error_mesage": self.error_message} - - else: - self.summary_dict = { - "Last modified": self.last_modified, - "File size": internal_tools.sizeof(self.file_size), - "Z-size": str(self.zsize), - "Y-size": str(self.ysize), - "X-size": str(self.xsize), - "Data type": self.data_type, - "Min value": self.min_value, - "Mean value": self.mean_intensity, - "Max value": self.max_value, - } - - def summary_str(self): - if "error_mesage" in self.summary_dict: - error_box = ouf.boxtitle("ERROR", return_str=True) - return f"{error_box}\n{self.summary_dict['error_mesage']}" - else: - display_dict = {k: v for k, v in self.summary_dict.items() if v is not None} - return ouf.showdict(display_dict, return_str=True, title="Data summary") - def zslice_from_zpos(self): self.zslice = int(self.zpos * (self.zsize - 1)) @@ -299,61 +380,125 @@ class Pipeline: self.figsize = 8 # Used for matplotlig figure size self.display_saturation_percentile = 99 self.verbose = False + self.session = None - def process_input(self, *args): - session = args[0] - session.data_path = args[1] - - # Get info from Tiff file - session.get_data_info() - session.create_summary_dict() - - # Memory map data as a virtual stack - + def load_data(self, session): try: session.vol = load( - session.data_path, virtual_stack=True, dataset_name=session.dataset_name + session.file_path, + virtual_stack=session.virtual_stack, + dataset_name=session.dataset_name, ) - except: - return session + except Exception as error_message: + raise ValueError( + f"Failed to load the image: {error_message}" + ) from error_message - if self.verbose: - log.info(ouf.br(3, return_str=True) + session.summary_str()) + session = self.get_data_info(session) return session - def show_summary_str(self, session): - session.create_summary_dict() - return session.summary_str() + def get_data_info(self, session): + first_slice = session.vol[0] - def create_zslice_fig(self, session): - slice_fig = self.create_slice_fig("z", session) + # Get info + session.zsize = len(session.vol) + session.ysize, session.xsize = first_slice.shape + session.data_type = str(first_slice.dtype) + session.last_modified = datetime.datetime.fromtimestamp( + os.path.getmtime(session.file_path) + ).strftime("%Y-%m-%d %H:%M") + session.file_size = os.path.getsize(session.file_path) + + return session + + def run_pipeline(self, session): + self.session = session + outputs = [] + log.info(session.all_operations) + for operation in session.all_operations: + if operation in session.operations: + outputs.append(self.run_operation(operation)) + + else: + log.info(f"Skipping {operation}") + outputs.append(None) + + return outputs + + def run_operation(self, operation): + log.info(f"Running {operation}") + + if operation == "Data summary": + return self.show_data_summary() + + if operation == "Z Slicer": + return self.create_zslice_fig() + + if operation == "Y Slicer": + return self.create_yslice_fig() + + if operation == "X Slicer": + return self.create_xslice_fig() + + if operation == "Z max projection": + return self.create_projections_figs()[0] + + if operation == "Z min projection": + return self.create_projections_figs()[1] + + if operation == "Intensity histogram": + return self.plot_vol_histogram() + + # In case nothing was triggered, raise error + raise ValueError("Unknown operation") + + def show_data_summary(self): + # Get info from Tiff file + + summary_dict = { + "Last modified": self.session.last_modified, + "File size": internal_tools.sizeof(self.session.file_size), + "Z-size": str(self.session.zsize), + "Y-size": str(self.session.ysize), + "X-size": str(self.session.xsize), + "Data type": self.session.data_type, + "Min value": self.session.min_value, + "Mean value": self.session.mean_intensity, + "Max value": self.session.max_value, + } + + display_dict = {k: v for k, v in summary_dict.items() if v is not None} + return ouf.showdict(display_dict, return_str=True, title="Data summary") + + def create_zslice_fig(self): + slice_fig = self.create_slice_fig("z") return slice_fig - def create_yslice_fig(self, session): - slice_fig = self.create_slice_fig("y", session) + def create_yslice_fig(self): + slice_fig = self.create_slice_fig("y") return slice_fig - def create_xslice_fig(self, session): - slice_fig = self.create_slice_fig("x", session) + def create_xslice_fig(self): + slice_fig = self.create_slice_fig("x") return slice_fig - def create_slice_fig(self, axis, session): + def create_slice_fig(self, axis): plt.close() - vol = session.vol - plt.set_cmap(session.cmap) + vol = self.session.vol + plt.set_cmap(self.session.cmap) - zslice = session.zslice_from_zpos() - yslice = session.yslice_from_ypos() - xslice = session.xslice_from_xpos() + zslice = self.session.zslice_from_zpos() + yslice = self.session.yslice_from_ypos() + xslice = self.session.xslice_from_xpos() # Check if we something to use as vmin and vmax - if session.min_percentile and session.max_percentile: - vmin = session.min_percentile - vmax = session.max_percentile + if self.session.min_percentile and self.session.max_percentile: + vmin = self.session.min_percentile + vmax = self.session.max_percentile else: vmin = None vmax = None @@ -393,55 +538,65 @@ class Pipeline: return fig - def create_projections_figs(self, session): - vol = session.vol + def create_projections_figs(self): + vol = self.session.vol - # Run projections - max_projection, min_projection = self.get_projections(vol, session) + if not self.session.projections_calculated: + projections = self.get_projections(vol) + self.session.max_projection = projections[0] + self.session.min_projection = projections[1] # Generate figures max_projection_fig = self.create_img_fig( - max_projection, - vmin=session.min_percentile, - vmax=session.max_percentile, + self.session.max_projection, + vmin=self.session.min_percentile, + vmax=self.session.max_percentile, ) min_projection_fig = self.create_img_fig( - min_projection, - vmin=session.min_percentile, - vmax=session.max_percentile, + self.session.min_projection, + vmin=self.session.min_percentile, + vmax=self.session.max_percentile, ) - return session, max_projection_fig, min_projection_fig - def get_projections(self, vol, session): + self.session.projections_calculated = True + return max_projection_fig, min_projection_fig + + def get_projections(self, vol): # Create arrays for iteration max_projection = np.zeros(np.shape(vol[0])) min_projection = np.ones(np.shape(vol[0])) * float("inf") intensity_sum = 0 - # Iterate over slices + # Iterate over slices. This is needed in case of virtual stacks. for zslice in vol: max_projection = np.maximum(max_projection, zslice) min_projection = np.minimum(min_projection, zslice) intensity_sum += np.sum(zslice) - session.min_value = np.min(min_projection) - session.min_percentile = np.percentile( + self.session.min_value = np.min(min_projection) + self.session.min_percentile = np.percentile( min_projection, 100 - self.display_saturation_percentile ) - session.max_value = np.max(max_projection) - session.max_percentile = np.percentile( + self.session.max_value = np.max(max_projection) + self.session.max_percentile = np.percentile( max_projection, self.display_saturation_percentile ) - session.intensity_sum = intensity_sum + self.session.intensity_sum = intensity_sum + + nvoxels = self.session.zsize * self.session.ysize * self.session.xsize + self.session.mean_intensity = intensity_sum / nvoxels - nvoxels = session.zsize * session.ysize * session.xsize - session.mean_intensity = intensity_sum / nvoxels return max_projection, min_projection - def plot_vol_histogram(self, session): + def plot_vol_histogram(self): + + # The Histogram needs results from the projections + if not self.session.projections_calculated: + _ = self.get_projections(self.session.vol) + vol_hist, bin_edges = self.vol_histogram( - session.vol, session.nbins, session.min_value, session.max_value + self.session.vol, self.session.nbins, self.session.min_value, self.session.max_value ) fig, ax = plt.subplots(figsize=(6, 4))