From e8ecc95c52074312db6b489ef8a91b4f563cc9da Mon Sep 17 00:00:00 2001 From: Felipe Delestro Matos <fima@pop-os.localdomain> Date: Mon, 15 May 2023 12:46:51 +0200 Subject: [PATCH] basic repo organization --- .gitignore | 17 ++ qim/__init__.py | 0 qim/gui/__init__.py | 0 qim/gui/dataexplorer.py | 451 ++++++++++++++++++++++++++++++++++++ qim/io/__init__.py | 2 + qim/io/load.py | 2 + qim/io/save.py | 2 + qim/tools/__init__.py | 0 qim/tools/internal_tools.py | 61 +++++ requirements.txt | 70 ++++++ setup.py | 31 +++ 11 files changed, 636 insertions(+) create mode 100644 .gitignore create mode 100644 qim/__init__.py create mode 100644 qim/gui/__init__.py create mode 100644 qim/gui/dataexplorer.py create mode 100644 qim/io/__init__.py create mode 100644 qim/io/load.py create mode 100644 qim/io/save.py create mode 100644 qim/tools/__init__.py create mode 100644 qim/tools/internal_tools.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1ede23b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Compiled Python files +*.pyc +*.pyo +__pycache__/ + +# Distribution directories +dist/ +build/ +*.egg-info/ + +# Development and editor files +.vscode/ +.idea/ +*.swp +*.swo +*.pyc +*~ \ No newline at end of file diff --git a/qim/__init__.py b/qim/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/qim/gui/__init__.py b/qim/gui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/qim/gui/dataexplorer.py b/qim/gui/dataexplorer.py new file mode 100644 index 00000000..ae76c1a8 --- /dev/null +++ b/qim/gui/dataexplorer.py @@ -0,0 +1,451 @@ +import gradio as gr +import numpy as np +import os +from ..tools import internal_tools +import tifffile +import outputformat as ouf +import datetime +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + + +class AppInterface: + def __init__(self): + self.show_header = False + self.verbose = True + self.title = "Data Explorer" + self.port = internal_tools.port_from_str(os.path.basename(__file__).split(".")[0]) + # CSS path + self.home = os.path.expanduser("~") + self.css_path = os.path.join(self.home, "qim-app/app/static/css/gradio.css") + + def clear(self): + """Used to reset outputs with the clear button""" + return None + + def create_interface(self): + with gr.Blocks(css=self.css_path) as gradio_interface: + gr.Markdown("# Data Explorer \n Quick insights from large datasets") + + with gr.Row(): + data_path = gr.Textbox( + value="/home/fima/Downloads/MarineGatropod_1.tif", + max_lines=1, + label="Path to the 3D volume", + ) + 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.Row(): + data_summary = gr.Text( + label=None, show_label=False, elem_classes="monospace-box" + ) + with gr.Column(): + 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(): + 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(): + 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() + + 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" + ) + + hist_plot = gr.Plot(label="Volume intensity histogram") + + pipeline = Pipeline() + pipeline.verbose = self.verbose + session = gr.State([]) + + with gr.Row(): + gr.Markdown("## Local thickness") + with gr.Row(): + gr.Plot() + gr.Plot() + gr.Plot() + + with gr.Row(): + gr.Markdown("## Structure tensor") + with gr.Row(): + gr.Plot() + gr.Plot() + gr.Plot() + + ### Gradio objects lists + + # Inputs + inputs = [zpos, ypos, xpos] + # Outputs + outputs = [ + data_summary, + zslice_plot, + yslice_plot, + xslice_plot, + max_projection_plot, + min_projection_plot, + ] + + 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 + # fmt: off + btn_run.click( + fn=self.start_session, inputs=inputs, outputs=session).then( + fn=pipeline.process_input, inputs=[session, data_path], outputs=session).then( + fn=pipeline.show_summary_str, inputs=session, outputs=data_summary).then( + fn=pipeline.create_zslice_fig, inputs=session, outputs=zslice_plot).then( + fn=pipeline.create_yslice_fig, inputs=session, outputs=yslice_plot).then( + fn=pipeline.create_xslice_fig, inputs=session, outputs=xslice_plot).then( + fn=pipeline.create_projections_figs, inputs=session, outputs=projection_outputs).then( + fn=pipeline.show_summary_str, inputs=session, outputs=data_summary).then( + fn=pipeline.plot_vol_histogram, inputs=session, outputs=hist_plot) + + + zpos.release( + fn=self.update_zpos, inputs=[session, zpos], outputs=[session, zslice_plot]).then( + fn=pipeline.create_zslice_fig, inputs=session, outputs=zslice_plot,show_progress=False) + ypos.release( + fn=self.update_ypos, inputs=[session, ypos], outputs=[session, yslice_plot]).then( + fn=pipeline.create_yslice_fig, inputs=session, outputs=yslice_plot,show_progress=False) + + xpos.release( + fn=self.update_xpos, inputs=[session, xpos], outputs=[session, xslice_plot]).then( + fn=pipeline.create_xslice_fig, inputs=session, outputs=xslice_plot,show_progress=False) + + # fmt: on + + return gradio_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] + + return session + + def update_zpos(self, session, zpos): + session.zpos = zpos + session.zslice_from_zpos() + + return session, gr.update(label=f"Z slice: {session.zslice}") + + def update_ypos(self, session, ypos): + session.ypos = ypos + session.yslice_from_ypos() + + return session, gr.update(label=f"Y slice: {session.yslice}") + + def update_xpos(self, session, xpos): + session.xpos = xpos + session.xslice_from_xpos() + + return session, gr.update(label=f"X slice: {session.xslice}") + + def launch(self, default_port=False, quiet=True, **kwargs): + # Show header + if self.show_header: + apptools.gradio_header(self.title, self.port) + + # Create gradio interfaces + interface = self.create_interface() + + if self.verbose: + quiet = False + + if default_port: + port = self.port + else: + port = None + + interface.launch(server_port=port, quiet=quiet, **kwargs) + + +class Session: + def __init__(self): + self.interface = None + self.data_path = None + self.vol = None + self.zpos = 0.5 + self.ypos = 0.5 + self.xpos = 0.5 + + # Volume info + self.zsize = None + self.ysize = None + self.xsize = None + self.data_type = None + self.axes = None + self.last_modified = None + self.file_size = None + self.min_percentile = None + self.max_percentile = None + self.min_value = None + self.max_value = None + self.intensity_sum = None + self.mean_intensity = None + + # Histogram + self.nbins = 32 + + def get_data_info(self): + # Open file + tif = tifffile.TiffFile(self.data_path) + first_slice = tif.pages[0] + + # Get info + self.zsize = len(tif.pages) + self.ysize, self.xsize = first_slice.shape + self.data_type = first_slice.dtype + self.axes = tif.series[0].axes + 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) + + # Close file + tif.close() + + def create_summary_dict(self): + # Create dictionary + self.summary_dict = { + "Last modified": self.last_modified, + "File size": apptools.sizeof(self.file_size), + "Axes": self.axes, + "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): + 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)) + + return self.zslice + + def yslice_from_ypos(self): + self.yslice = int(self.ypos * (self.ysize - 1)) + + return self.yslice + + def xslice_from_xpos(self): + self.xslice = int(self.xpos * (self.xsize - 1)) + + return self.xslice + + +class Pipeline: + def __init__(self): + self.figsize = 8 # Used for matplotlig figure size + self.display_saturation_percentile = 99 + self.verbose = False + + 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 + session.vol = tifffile.memmap(session.data_path) + + if self.verbose: + print(ouf.br(3, return_str=True) + session.summary_str()) + + return session + + def show_summary_str(self, session): + session.create_summary_dict() + return session.summary_str() + + def create_zslice_fig(self, session): + slice_fig = self.create_slice_fig("z", session) + + return slice_fig + + def create_yslice_fig(self, session): + slice_fig = self.create_slice_fig("y", session) + + return slice_fig + + def create_xslice_fig(self, session): + slice_fig = self.create_slice_fig("x", session) + + return slice_fig + + def create_slice_fig(self, axis, session): + plt.close() + vol = session.vol + + zslice = session.zslice_from_zpos() + yslice = session.yslice_from_ypos() + xslice = 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 + else: + vmin = None + vmax = None + + if axis == "z": + slice_fig = self._zslice_fig(vol, zslice, vmin=vmin, vmax=vmax) + if axis == "y": + slice_fig = self._yslice_fig(vol, yslice, vmin=vmin, vmax=vmax) + if axis == "x": + slice_fig = self._xslice_fig(vol, xslice, vmin=vmin, vmax=vmax) + + return slice_fig + + def _zslice_fig(self, vol, slice, **kwargs): + fig = self.create_img_fig(vol[slice, :, :], **kwargs) + + return fig + + def _yslice_fig(self, vol, slice, **kwargs): + fig = self.create_img_fig(vol[:, slice, :], **kwargs) + + return fig + + def _xslice_fig(self, vol, slice, **kwargs): + fig = self.create_img_fig(vol[:, :, slice], **kwargs) + + return fig + + def create_img_fig(self, img, **kwargs): + fig, ax = plt.subplots(figsize=(self.figsize, self.figsize)) + + ax.imshow(img, interpolation="nearest", **kwargs) + + # Adjustments + ax.axis("off") + fig.subplots_adjust(left=0, right=1, bottom=0, top=1) + + return fig + + def create_projections_figs(self, session): + vol = session.vol + + # Run projections + max_projection, min_projection = self.get_projections(vol, session) + + # Generate figures + max_projection_fig = self.create_img_fig( + max_projection, + vmin=session.min_percentile, + vmax=session.max_percentile, + ) + min_projection_fig = self.create_img_fig( + min_projection, + vmin=session.min_percentile, + vmax=session.max_percentile, + ) + return session, max_projection_fig, min_projection_fig + + def get_projections(self, vol, session): + # 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 + 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( + min_projection, 100 - self.display_saturation_percentile + ) + session.max_value = np.max(max_projection) + session.max_percentile = np.percentile( + max_projection, self.display_saturation_percentile + ) + + session.intensity_sum = intensity_sum + + nvoxels = session.zsize * session.ysize * session.xsize + session.mean_intensity = intensity_sum / nvoxels + return max_projection, min_projection + + def plot_vol_histogram(self, session): + vol_hist, bin_edges = self.vol_histogram( + session.vol, session.nbins, session.min_value, session.max_value + ) + + fig, ax = plt.subplots(figsize=(6, 4)) + + ax.bar( + bin_edges[:-1], vol_hist, width=np.diff(bin_edges), ec="white", align="edge" + ) + + # Adjustments + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["left"].set_visible(True) + ax.spines["bottom"].set_visible(True) + ax.set_yscale("log") + + # fig.subplots_adjust(left=0, right=1, bottom=0, top=1) + + return fig + + def vol_histogram(self, vol, nbins, min_value, max_value): + # Start histogram + vol_hist = np.zeros(nbins) + + # Iterate over slices + for zslice in vol: + hist, bin_edges = np.histogram( + zslice, bins=nbins, range=(min_value, max_value) + ) + vol_hist += hist + + return vol_hist, bin_edges + + +if __name__ == "__main__": + app = AppInterface() + app.show_header = True + app.launch(server_name="0.0.0.0", show_error=True, default_port=False) diff --git a/qim/io/__init__.py b/qim/io/__init__.py new file mode 100644 index 00000000..1a34a4c0 --- /dev/null +++ b/qim/io/__init__.py @@ -0,0 +1,2 @@ +from .load import load +from .save import save \ No newline at end of file diff --git a/qim/io/load.py b/qim/io/load.py new file mode 100644 index 00000000..f28dedbb --- /dev/null +++ b/qim/io/load.py @@ -0,0 +1,2 @@ +def load(): + print ("hello from load") \ No newline at end of file diff --git a/qim/io/save.py b/qim/io/save.py new file mode 100644 index 00000000..4bad5b30 --- /dev/null +++ b/qim/io/save.py @@ -0,0 +1,2 @@ +def save(): + print ("hello from save") \ No newline at end of file diff --git a/qim/tools/__init__.py b/qim/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/qim/tools/internal_tools.py b/qim/tools/internal_tools.py new file mode 100644 index 00000000..fe77e4c1 --- /dev/null +++ b/qim/tools/internal_tools.py @@ -0,0 +1,61 @@ +import socket +import hashlib + + +def mock_plot(): + import matplotlib.pyplot as plt + from matplotlib.figure import Figure + import numpy as np + import matplotlib + + matplotlib.use("Agg") + + fig = plt.figure(figsize=(5, 4)) + ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) + xx = np.arange(0, 2 * np.pi, 0.01) + ax.plot(xx, np.sin(xx)) + + return fig + + +def mock_write_file(path): + f = open(path, "w") + f.write("File created by apptools") + f.close() + + +def get_local_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + # doesn't even have to be reachable + s.connect(("192.255.255.255", 1)) + IP = s.getsockname()[0] + except: + IP = "127.0.0.1" + finally: + s.close() + return IP + + +def port_from_str(s): + return int(hashlib.sha1(s.encode("utf-8")).hexdigest(), 16) % (10**4) + + +def gradio_header(title, port): + import outputformat as ouf + + ouf.br(2) + details = [ + f'{ouf.c(title, color="rainbow", cmap="cool", bold=True, return_str=True)}', + f"Using port {port}", + f"Running at {get_local_ip()}", + ] + ouf.showlist(details, style="box", title="Starting gradio server") + + +def sizeof(num, suffix="B"): + for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]: + if abs(num) < 1024.0: + return f"{num:3.1f} {unit}{suffix}" + num /= 1024.0 + return f"{num:.1f} Y{suffix}" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..dc12c343 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,70 @@ +aiofiles==23.1.0 +aiohttp==3.8.4 +aiosignal==1.3.1 +altair==5.0.0 +anyio==3.6.2 +async-timeout==4.0.2 +attrs==23.1.0 +certifi==2023.5.7 +charset-normalizer==3.1.0 +click==8.1.3 +contourpy==1.0.7 +cycler==0.11.0 +edt==2.3.1 +fastapi==0.95.1 +ffmpy==0.3.0 +filelock==3.12.0 +fonttools==4.39.4 +frozenlist==1.3.3 +fsspec==2023.5.0 +gradio==3.30.0 +gradio_client==0.2.4 +h11==0.14.0 +httpcore==0.17.0 +httpx==0.24.0 +huggingface-hub==0.14.1 +idna==3.4 +Jinja2==3.1.2 +jsonschema==4.17.3 +kiwisolver==1.4.4 +linkify-it-py==2.0.2 +localthickness==0.1.2 +markdown-it-py==2.2.0 +MarkupSafe==2.1.2 +matplotlib==3.7.1 +mdit-py-plugins==0.3.3 +mdurl==0.1.2 +multidict==6.0.4 +numpy==1.24.3 +orjson==3.8.12 +outputformat==0.1.3 +packaging==23.1 +pandas==2.0.1 +Pillow==9.5.0 +plotly==5.14.1 +pydantic==1.10.7 +pydub==0.25.1 +Pygments==2.15.1 +pyparsing==3.0.9 +pyrsistent==0.19.3 +python-dateutil==2.8.2 +python-multipart==0.0.6 +pytz==2023.3 +PyYAML==6.0 +requests==2.30.0 +scipy==1.10.1 +semantic-version==2.10.0 +six==1.16.0 +sniffio==1.3.0 +starlette==0.26.1 +tenacity==8.2.2 +tifffile==2023.4.12 +toolz==0.12.0 +tqdm==4.65.0 +typing_extensions==4.5.0 +tzdata==2023.3 +uc-micro-py==1.0.2 +urllib3==2.0.2 +uvicorn==0.22.0 +websockets==11.0.3 +yarl==1.9.2 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..91b3958e --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +from setuptools import setup, find_packages + +# Read the contents of your README file +with open("README.md", "r", encoding="utf-8") as f: + long_description = f.read() + +setup( + name="qim", + version="0.1.0", + author="Felipe Delestro", + author_email="fima@dtu.dk", + description="QIM tools and user interfaces", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://lab.compute.dtu.dk/QIM/qim", + packages=find_packages(), + classifiers=[ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Development Status :: 1 - Planning", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Image Processing", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: User Interfaces", + ], + python_requires=">=3.6", + install_requires=open("requirements.txt").readlines(), +) -- GitLab