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