From a095e0e898d13ec4e82c0f9ed4b5bed4f5ffb775 Mon Sep 17 00:00:00 2001 From: jhnnsrs Date: Fri, 21 Apr 2023 17:22:18 +0200 Subject: [PATCH] build again have fuuun --- graphql/stage.graphql | 30 ++++++ gucker/api/schema.py | 211 ++++++++++++++++++++++++++++++++++++++++++ gucker/main.py | 78 ++++++++++++++-- poetry.lock | 39 ++++---- pyproject.toml | 2 +- 5 files changed, 333 insertions(+), 27 deletions(-) create mode 100644 graphql/stage.graphql create mode 100644 gucker/api/schema.py diff --git a/graphql/stage.graphql b/graphql/stage.graphql new file mode 100644 index 0000000..5d38ac5 --- /dev/null +++ b/graphql/stage.graphql @@ -0,0 +1,30 @@ +fragment ExportStage on Stage { + name + positions { + name + id + omeros { + acquisitionDate + representation { + store + name + id + fileOrigins { + id + file + } + derived(flatten: 4) { + id + store + name + } + } + } + } +} + +query GetExportStage($id: ID!) { + stage(id: $id) { + ...ExportStage + } +} diff --git a/gucker/api/schema.py b/gucker/api/schema.py new file mode 100644 index 0000000..a263494 --- /dev/null +++ b/gucker/api/schema.py @@ -0,0 +1,211 @@ +from rath.scalars import ID +from mikro.funcs import execute, aexecute +from mikro.traits import Position, Representation, Omero, Stage +from enum import Enum +from mikro.scalars import Store, File +from datetime import datetime +from pydantic import BaseModel, Field +from typing import Optional, Literal, Tuple +from mikro.rath import MikroRath + + +class ExportStageFragmentPositionsOmerosRepresentationFileorigins(BaseModel): + typename: Optional[Literal["OmeroFile"]] = Field(alias="__typename", exclude=True) + id: ID + file: Optional[File] + "The file" + + class Config: + frozen = True + + +class ExportStageFragmentPositionsOmerosRepresentationDerived( + Representation, BaseModel +): + """A Representation is 5-dimensional representation of an image + + Mikro stores each image as sa 5-dimensional representation. The dimensions are: + - t: time + - c: channel + - z: z-stack + - x: x-dimension + - y: y-dimension + + This ensures a unified api for all images, regardless of their original dimensions. Another main + determining factor for a representation is its variety: + A representation can be a raw image representating voxels (VOXEL) + or a segmentation mask representing instances of a class. (MASK) + It can also representate a human perception of the image (RGB) or a human perception of the mask (RGBMASK) + + # Meta + + Meta information is stored in the omero field which gives access to the omero-meta data. Refer to the omero documentation for more information. + + + #Origins and Derivations + + Images can be filtered, which means that a new representation is created from the other (original) representations. This new representation is then linked to the original representations. This way, we can always trace back to the original representation. + Both are encapsulaed in the origins and derived fields. + + Representations belong to *one* sample. Every transaction to our image data is still part of the original acuqistion, so also filtered images are refering back to the sample + Each iamge has also a name, which is used to identify the image. The name is unique within a sample. + File and Rois that are used to create images are saved in the file origins and roi origins repectively. + + + """ + + typename: Optional[Literal["Representation"]] = Field( + alias="__typename", exclude=True + ) + id: ID + store: Optional[Store] + name: Optional[str] + "Cleartext name" + + class Config: + frozen = True + + +class ExportStageFragmentPositionsOmerosRepresentation(Representation, BaseModel): + """A Representation is 5-dimensional representation of an image + + Mikro stores each image as sa 5-dimensional representation. The dimensions are: + - t: time + - c: channel + - z: z-stack + - x: x-dimension + - y: y-dimension + + This ensures a unified api for all images, regardless of their original dimensions. Another main + determining factor for a representation is its variety: + A representation can be a raw image representating voxels (VOXEL) + or a segmentation mask representing instances of a class. (MASK) + It can also representate a human perception of the image (RGB) or a human perception of the mask (RGBMASK) + + # Meta + + Meta information is stored in the omero field which gives access to the omero-meta data. Refer to the omero documentation for more information. + + + #Origins and Derivations + + Images can be filtered, which means that a new representation is created from the other (original) representations. This new representation is then linked to the original representations. This way, we can always trace back to the original representation. + Both are encapsulaed in the origins and derived fields. + + Representations belong to *one* sample. Every transaction to our image data is still part of the original acuqistion, so also filtered images are refering back to the sample + Each iamge has also a name, which is used to identify the image. The name is unique within a sample. + File and Rois that are used to create images are saved in the file origins and roi origins repectively. + + + """ + + typename: Optional[Literal["Representation"]] = Field( + alias="__typename", exclude=True + ) + store: Optional[Store] + name: Optional[str] + "Cleartext name" + id: ID + file_origins: Tuple[ + ExportStageFragmentPositionsOmerosRepresentationFileorigins, ... + ] = Field(alias="fileOrigins") + derived: Optional[ + Tuple[Optional[ExportStageFragmentPositionsOmerosRepresentationDerived], ...] + ] + "Derived Images from this Image" + + class Config: + frozen = True + + +class ExportStageFragmentPositionsOmeros(Omero, BaseModel): + """Omero is a through model that stores the real world context of an image + + This means that it stores the position (corresponding to the relative displacement to + a stage (Both are models)), objective and other meta data of the image. + + """ + + typename: Optional[Literal["Omero"]] = Field(alias="__typename", exclude=True) + acquisition_date: Optional[datetime] = Field(alias="acquisitionDate") + representation: ExportStageFragmentPositionsOmerosRepresentation + + class Config: + frozen = True + + +class ExportStageFragmentPositions(Position, BaseModel): + """The relative position of a sample on a microscope stage""" + + typename: Optional[Literal["Position"]] = Field(alias="__typename", exclude=True) + name: str + "The name of the possition" + id: ID + omeros: Optional[Tuple[Optional[ExportStageFragmentPositionsOmeros], ...]] + "Associated images through Omero" + + class Config: + frozen = True + + +class ExportStageFragment(Stage, BaseModel): + typename: Optional[Literal["Stage"]] = Field(alias="__typename", exclude=True) + name: str + "The name of the stage" + positions: Tuple[ExportStageFragmentPositions, ...] + + class Config: + frozen = True + + +class GetExportStageQuery(BaseModel): + stage: Optional[ExportStageFragment] + 'Get a single experiment by ID"\n \n Returns a single experiment by ID. If the user does not have access\n to the experiment, an error will be raised.\n \n ' + + class Arguments(BaseModel): + id: ID + + class Meta: + document = "fragment ExportStage on Stage {\n name\n positions {\n name\n id\n omeros {\n acquisitionDate\n representation {\n store\n name\n id\n fileOrigins {\n id\n file\n }\n derived(flatten: 4) {\n id\n store\n name\n }\n }\n }\n }\n}\n\nquery GetExportStage($id: ID!) {\n stage(id: $id) {\n ...ExportStage\n }\n}" + + +async def aget_export_stage( + id: ID, rath: MikroRath = None +) -> Optional[ExportStageFragment]: + """GetExportStage + + + stage: An Stage is a set of positions that share a common space on a microscope and can + be use to translate. + + + + + + Arguments: + id (ID): id + rath (mikro.rath.MikroRath, optional): The mikro rath client + + Returns: + Optional[ExportStageFragment]""" + return (await aexecute(GetExportStageQuery, {"id": id}, rath=rath)).stage + + +def get_export_stage(id: ID, rath: MikroRath = None) -> Optional[ExportStageFragment]: + """GetExportStage + + + stage: An Stage is a set of positions that share a common space on a microscope and can + be use to translate. + + + + + + Arguments: + id (ID): id + rath (mikro.rath.MikroRath, optional): The mikro rath client + + Returns: + Optional[ExportStageFragment]""" + return execute(GetExportStageQuery, {"id": id}, rath=rath).stage diff --git a/gucker/main.py b/gucker/main.py index d35d657..b50dec7 100644 --- a/gucker/main.py +++ b/gucker/main.py @@ -19,7 +19,9 @@ from arkitekt.builders import publicqt from arkitekt import log import logging - +from gucker.api.schema import get_export_stage +from mikro import Stage, Image +import tifffile logger = logging.getLogger(__name__) @@ -46,6 +48,7 @@ def __init__(self, **kwargs) -> None: self.setStyleSheet("background-color: #1e1e1e; color: #ffffff;") self.settings = QtCore.QSettings("Gucker", "gg") self.base_dir = self.settings.value("base_dir", "") + self.export_dir = self.settings.value("export_dir", "") self.is_watching.connect(self.is_watching_changed) self.is_uploading.connect(self.is_uploading_changed) @@ -70,6 +73,8 @@ def __init__(self, **kwargs) -> None: ) self.button = QtWidgets.QPushButton("Select Directory to watch") self.button.clicked.connect(self.on_base_dir) + self.exportbutton = QtWidgets.QPushButton("Select Directory to export") + self.exportbutton.clicked.connect(self.on_export_dir) if self.base_dir == "": self.button.setText("Select Watching Folder") @@ -86,28 +91,53 @@ def __init__(self, **kwargs) -> None: layout = QtWidgets.QVBoxLayout() layout.addWidget(self.center_label) layout.addWidget(self.button) + layout.addWidget(self.exportbutton) layout.addWidget(self.magic_bar) self.centralWidget.setLayout(layout) self.setCentralWidget(self.centralWidget) # self.app.rekuest.register(on_provide=self.on_stream_provide)(self.stream_folder) self.app.rekuest.register()(self.stream_files) + self.app.rekuest.register()(self.export_stage) self.setWindowTitle("Gucker") def on_base_dir(self): self.base_dir = QtWidgets.QFileDialog.getExistingDirectory( - self, "Select Folder" + self, "Select Stream Folder" ) if self.base_dir: - self.button.setText(f"Selected {self.base_dir}") - self.magic_bar.magicb.setText("Select Folder first") self.settings.setValue("base_dir", self.base_dir) - self.magic_bar.magicb.setDisabled(False) + self.button.setText(f"Selected {self.base_dir}") else: self.button.setText("Select Watching Folder") - self.settings.setValue("base_dir", "") + + self.check_folders_sane() + + def check_folders_sane(self): + if not self.base_dir: + self.statusBar.showMessage("Select a folder to watch first") + self.magic_bar.magicb.setDisabled(True) + return False + if not self.export_dir: + self.statusBar.showMessage("Select a folder to export first") self.magic_bar.magicb.setDisabled(True) - self.magic_bar.magicb.setText("Select Folder first") + return False + + self.statusBar.showMessage("All set ready to go") + self.magic_bar.magicb.setDisabled(False) + return True + + def on_export_dir(self): + self.export_dir = QtWidgets.QFileDialog.getExistingDirectory( + self, "Select Export Folder" + ) + if self.export_dir: + self.settings.setValue("export_dir", self.export_dir) + self.exportbutton.setText(f"Selected {self.export_dir}") + else: + self.exportbutton.setText("Select Export Folder") + + self.check_folders_sane() def is_watching_changed(self, select) -> None: if select: @@ -199,6 +229,40 @@ def stream_files( self.is_watching.emit(False) + def export_representation(self, representation: Image, dir: str) -> None: + tifffile.imsave( + os.path.join(dir, f"ID({representation.id}) {representation.name}.tiff"), + representation.data, + ) + + def export_stage(self, stage: Stage) -> None: + """Export Stage + + Exports the stage to the export directory + + Args: + stage (Stage): The stage to export + """ + assert self.export_dir, "No export directory selected" + export_stage = get_export_stage(stage) + print(export_stage) + + stage_dir = os.path.join(self.export_dir, f"ID({stage.id}) {export_stage.name}") + os.makedirs(stage_dir, exist_ok=True) + for item in export_stage.positions: + pos_dir = os.path.join(stage_dir, f"ID({item.id}) {item.name}") + os.makedirs(pos_dir, exist_ok=True) + for image in item.omeros: + image_dir = os.path.join( + pos_dir, + f"ID({image.representation.id}) {image.representation.name} {image.acquisition_date}", + ) + os.makedirs(image_dir, exist_ok=True) + self.export_representation(image.representation, image_dir) + + for file in image.representation.derived: + self.export_representation(file, image_dir) + def main(**kwargs) -> None: """Entrypoint for the application""" diff --git a/poetry.lock b/poetry.lock index cf3d293..9982819 100644 --- a/poetry.lock +++ b/poetry.lock @@ -211,26 +211,27 @@ trio = ["trio (>=0.16,<0.22)"] [[package]] name = "arkitekt" -version = "0.4.83" +version = "0.4.86" description = "client for the arkitekt platform" category = "main" optional = false python-versions = ">=3.8,<=3.12" files = [ - {file = "arkitekt-0.4.83-py3-none-any.whl", hash = "sha256:eb61f1fa247cf9096304272aedad5096f5aac498d2079351b6a1532569953c61"}, - {file = "arkitekt-0.4.83.tar.gz", hash = "sha256:8c3bcd76d7ac17c145d7c3beb25c74b60ba589f3644d42adedf93c0cc7bc9749"}, + {file = "arkitekt-0.4.86-py3-none-any.whl", hash = "sha256:dfba1651e1cb247b879292ae938bea65f81399ec81f6ee5f66d6647f3fe4581e"}, + {file = "arkitekt-0.4.86.tar.gz", hash = "sha256:91e23a2f2d8374377715b262f89f0226b752983b86f9dd4e1a60bd4d0c540451"}, ] [package.dependencies] -fakts = ">=0.3.18" -fluss = ">=0.1.42" +fakts = ">=0.3.19" +fluss = ">=0.1.46" herre = ">=0.3.18" -mikro = ">=0.3.48" +mikro = ">=0.3.56" rekuest = ">=0.1.30" unlok = ">=0.1.12" [package.extras] cli = ["rich-click (>=1.6.1,<2.0.0)", "turms (>=0.3.1a0)", "watchfiles (>=0.18.1,<0.19.0)"] +scheduler = ["reaktion (>=0.1.18)"] [[package]] name = "asciitree" @@ -633,14 +634,14 @@ test = ["pytest (>=6)"] [[package]] name = "fakts" -version = "0.3.18" +version = "0.3.19" description = "asynchronous configuration provider ( tailored to support dynamic client-server relations)" category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "fakts-0.3.18-py3-none-any.whl", hash = "sha256:1f757fecd53bc46ef9c8fc68365454853afc3c1bd0f21c9eecc4c022fe33f175"}, - {file = "fakts-0.3.18.tar.gz", hash = "sha256:f26745327abebe650e13dca4ecbcf904a6084282ba5ad43dc340d4d86ab74426"}, + {file = "fakts-0.3.19-py3-none-any.whl", hash = "sha256:bc9b6f7be065eb61fa8b22c3e2a428ddeebbd22d4f4fe266b13d941e34695c25"}, + {file = "fakts-0.3.19.tar.gz", hash = "sha256:6fd8d17ee7951c03a85ebee2b89fdc9feb1f9cd498091e3cd962d44081a810b0"}, ] [package.dependencies] @@ -666,14 +667,14 @@ files = [ [[package]] name = "fluss" -version = "0.1.44" +version = "0.1.46" description = "" category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "fluss-0.1.44-py3-none-any.whl", hash = "sha256:4f038060cf92ed09d22dd972c9df32add847579628cbbe2d43d92a5fe07e236c"}, - {file = "fluss-0.1.44.tar.gz", hash = "sha256:d8dacfef584f5ac52ff19116064718f1ba029e5e4bd5a600bac652db0cc26d0a"}, + {file = "fluss-0.1.46-py3-none-any.whl", hash = "sha256:d06b28b3c6e5d449171d37c285102b05f18b4b39065594cb74a70de8151b7c46"}, + {file = "fluss-0.1.46.tar.gz", hash = "sha256:9ac0882d3c87f31c32161429fa393a5a8ecca14ddd3af412589f1b5faa5ef915"}, ] [package.dependencies] @@ -965,14 +966,14 @@ altgraph = ">=0.17" [[package]] name = "mikro" -version = "0.3.48" +version = "0.3.57" description = "images for arkitekt" category = "main" optional = false python-versions = ">=3.8,<=3.12" files = [ - {file = "mikro-0.3.48-py3-none-any.whl", hash = "sha256:b4652bff6f645e8bd0b4441a16070da289b19091150dc787f31c9f585d9abfa1"}, - {file = "mikro-0.3.48.tar.gz", hash = "sha256:a3cda23898652e50f30394b98779b4a480474272fd2be306b20d13df6bfc1dcc"}, + {file = "mikro-0.3.57-py3-none-any.whl", hash = "sha256:a15faf10eb778bd7d0a938619898f18662b510607c11853233e84771fcba583b"}, + {file = "mikro-0.3.57.tar.gz", hash = "sha256:67debc3eeed2cc77c9a290c559f74849f95453b895d59b1e8beef8c183c099da"}, ] [package.dependencies] @@ -1807,14 +1808,14 @@ boto3 = ["aiobotocore[boto3] (>=2.5.0,<2.6.0)"] [[package]] name = "setuptools" -version = "67.6.1" +version = "67.7.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, + {file = "setuptools-67.7.1-py3-none-any.whl", hash = "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67"}, + {file = "setuptools-67.7.1.tar.gz", hash = "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c"}, ] [package.extras] @@ -2288,4 +2289,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "6cc83a7b6c30d82503bc186821c9fbfc80fe2e5525cf91e99fbb7a4e6c3bf893" +content-hash = "6fd92b9d5ef421acbe89278319deffa8718fdd20b1b56b09b402a3bd035b570a" diff --git a/pyproject.toml b/pyproject.toml index 04038e1..464fec4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ packages = [{ include = "gucker" }] [tool.poetry.dependencies] python = ">=3.8,<3.12" -arkitekt = "^0.4.83" +arkitekt = "^0.4.68" [tool.mypy] exclude = ["venv/"]