plexora 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Opening a plugin's tool, and collecting whatever it still needs.
|
|
2
|
+
|
|
3
|
+
A plugin declares what it needs (plexora/api/plugin.py's Requires). Core works
|
|
4
|
+
out what the project already has, asks only for the difference, and stores the
|
|
5
|
+
answers on the project -- so the next plugin to want the same thing never asks
|
|
6
|
+
again. That is the whole mechanism: core never names a concrete plugin, and a
|
|
7
|
+
plugin never grows its own "please type a column name" box.
|
|
8
|
+
|
|
9
|
+
Opening a tool has three outcomes:
|
|
10
|
+
|
|
11
|
+
OPEN -- everything it needs is there.
|
|
12
|
+
COLLECT -- installed and compatible, but something is missing. Recoverable, so
|
|
13
|
+
the client is told what to ask for and re-enters here once it has
|
|
14
|
+
the answers. This is why the Tools menu lists compatible-but-not-
|
|
15
|
+
ready plugins at all: hiding them hides the only route to making
|
|
16
|
+
them work.
|
|
17
|
+
OFFER -- nothing is blocking, but there is something to offer once. A plugin
|
|
18
|
+
whose inputs are ALL optional would otherwise never reach the modal
|
|
19
|
+
at all: `optional_missing_from` exists and is documented as "offered
|
|
20
|
+
once", and without this outcome nothing ever consults it.
|
|
21
|
+
FALLBACK -- unknown datasource, uninstalled tool, or permanently incompatible.
|
|
22
|
+
Stale and bookmarked links land here, so it must not error.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from flask import jsonify, redirect, render_template, request
|
|
26
|
+
|
|
27
|
+
from plexora import app
|
|
28
|
+
from plexora.server import plugins as plugin_registry
|
|
29
|
+
from plexora.server.models import data_model
|
|
30
|
+
from plexora.server.models.project import ROLE_LABELS, ROLE_NAMES, Project
|
|
31
|
+
from plexora.server.routes.page_routes import template_data
|
|
32
|
+
|
|
33
|
+
OPEN, COLLECT, OFFER, FALLBACK = "open", "collect", "offer", "fallback"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve(datasource, tool_name):
|
|
37
|
+
"""(outcome, plugin, project) for opening `tool_name` on `datasource`.
|
|
38
|
+
|
|
39
|
+
Whether a tool applies is the plugin's own declaration, not a rule core
|
|
40
|
+
hardcodes -- core used to test `image_kind == 'rgb'` directly, which only
|
|
41
|
+
ever encoded what gating in particular could not handle.
|
|
42
|
+
"""
|
|
43
|
+
project = Project.find(datasource)
|
|
44
|
+
plugin = plugin_registry.find(app, tool_name)
|
|
45
|
+
if project is None or plugin is None or not plugin.requires.applies_to(project):
|
|
46
|
+
return FALLBACK, None, project
|
|
47
|
+
# Two reasons to collect, not one. Something absent is the obvious case; a
|
|
48
|
+
# value the column predictor guessed and nobody ever looked at is the other,
|
|
49
|
+
# and it is the more common one -- a conventionally-named table leaves
|
|
50
|
+
# nothing missing at all, so without this the first launch would silently
|
|
51
|
+
# run on five guesses.
|
|
52
|
+
if plugin.requires.missing_from(project) or plugin.requires.unconfirmed_from(project):
|
|
53
|
+
return COLLECT, plugin, project
|
|
54
|
+
# Nothing blocks, but something has never been put in front of the user.
|
|
55
|
+
# Kept as its own outcome rather than folded into COLLECT because the two
|
|
56
|
+
# callers below want opposite things from it: the panel fetch shows the
|
|
57
|
+
# form, the plain <a href> opens the tool. A plugin that requires nothing
|
|
58
|
+
# at all -- ROI -- reaches the modal only through here.
|
|
59
|
+
if plugin.requires.optional_missing_from(project):
|
|
60
|
+
return OFFER, plugin, project
|
|
61
|
+
return OPEN, plugin, project
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _needs(plugin, project):
|
|
65
|
+
"""What the client renders a form from.
|
|
66
|
+
|
|
67
|
+
Everything here is generic: a list of typed requirements plus the material
|
|
68
|
+
needed to answer them (the project's columns, its current roles). The
|
|
69
|
+
modal builds inputs from `kind` without knowing which plugin asked.
|
|
70
|
+
|
|
71
|
+
Three lists, because the user is in three different positions:
|
|
72
|
+
|
|
73
|
+
`missing` -- nothing is stored; the field starts empty.
|
|
74
|
+
`confirm` -- something is stored but was guessed; the field is prefilled
|
|
75
|
+
and the user is being shown it once.
|
|
76
|
+
`optional` -- absent and not blocking; offered while the form is open.
|
|
77
|
+
|
|
78
|
+
A requirement the user has already answered appears in none of them. That
|
|
79
|
+
is the property the whole design turns on, and it is why an answer is
|
|
80
|
+
recorded on the project rather than by the plugin that asked for it.
|
|
81
|
+
"""
|
|
82
|
+
from plexora.server.routes.project_routes import known_layers, known_obsm
|
|
83
|
+
|
|
84
|
+
# Filled in from the file for a project imported before layers or obsm were
|
|
85
|
+
# recorded, which is precisely the project whose matrix and coordinate
|
|
86
|
+
# choices are wrong and cannot be corrected. Deferred import:
|
|
87
|
+
# project_routes imports this module.
|
|
88
|
+
project = known_obsm(known_layers(project))
|
|
89
|
+
return {
|
|
90
|
+
"tool": plugin.name,
|
|
91
|
+
"label": plugin.label,
|
|
92
|
+
# Why anyone would fill in a form that blocks nothing. Core's own
|
|
93
|
+
# wording covers the blocking case for every plugin; this one cannot be
|
|
94
|
+
# written generically, so the plugin supplies it (Plugin.intro).
|
|
95
|
+
"intro": plugin.intro,
|
|
96
|
+
"missing": [r.describe() for r in plugin.requires.missing_from(project)],
|
|
97
|
+
"confirm": [r.describe() for r in plugin.requires.unconfirmed_from(project)],
|
|
98
|
+
"optional": [r.describe() for r in plugin.requires.optional_missing_from(project)],
|
|
99
|
+
"columns": {
|
|
100
|
+
"markers": list(project.columns.markers),
|
|
101
|
+
"metadata": list(project.columns.metadata),
|
|
102
|
+
},
|
|
103
|
+
# What a role select is made of: the columns to offer, the answer to
|
|
104
|
+
# preselect, and what a blank one falls back to. All three come from the
|
|
105
|
+
# project rather than being assembled client-side, because for AnnData
|
|
106
|
+
# and SpatialData the question is asked about the source file's obs
|
|
107
|
+
# columns and answered into the read spec -- a translation the modal has
|
|
108
|
+
# no business knowing about (see Project.role_columns).
|
|
109
|
+
"roleColumns": project.role_columns,
|
|
110
|
+
"roleAnswers": project.role_answers,
|
|
111
|
+
"roleDefaults": project.role_defaults,
|
|
112
|
+
# What the coordinate field is made of: every obsm array the file
|
|
113
|
+
# carries (with its shape), every obs column, and the source recorded
|
|
114
|
+
# now. Assembled here rather than client-side for the same reason the
|
|
115
|
+
# role keys are -- the answer is a read spec, not a column name.
|
|
116
|
+
"coordinateOptions": project.coordinate_options,
|
|
117
|
+
# Whether the user has said this table covers a single image, which is
|
|
118
|
+
# an answer to the image-id question and not an absence of one.
|
|
119
|
+
"singleImage": bool(project.dataset and project.dataset.single_image),
|
|
120
|
+
# The same thing for the cell id: whether the user has said to number
|
|
121
|
+
# the rows, which is an answer and not a blank left alone.
|
|
122
|
+
"rowNumberIds": bool(project.dataset and project.dataset.row_number_ids),
|
|
123
|
+
# What the "Expression values" field is made of: every matrix the file
|
|
124
|
+
# carries, which one is read now, and whether it is log1p'd on the way
|
|
125
|
+
# in. Same three keys the edit page renders from.
|
|
126
|
+
"featureOptions": project.feature_options,
|
|
127
|
+
"featureSource": project.feature_source,
|
|
128
|
+
"featureLog": project.log_transformed,
|
|
129
|
+
# The whole role vocabulary, so every surface words a role the same way
|
|
130
|
+
# and core owns the wording rather than each of them. The modal reads a
|
|
131
|
+
# label off the requirement itself and does not need this today; it is
|
|
132
|
+
# kept because a client asking what this project's roles are called
|
|
133
|
+
# should not have to know which of them happened to be asked for.
|
|
134
|
+
"roleLabels": dict(ROLE_LABELS),
|
|
135
|
+
"hasTable": project.has_table,
|
|
136
|
+
"segmentationPending": project.segmentation.pending,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.route('/<string:datasource>/tools/<string:tool_name>')
|
|
141
|
+
def open_tool(datasource, tool_name):
|
|
142
|
+
"""The plain <a href> path, for a client that did not intercept the click.
|
|
143
|
+
|
|
144
|
+
Hands off to the edit page rather than the upload page: the edit page is
|
|
145
|
+
generated from the same requirements, so there is one surface to maintain
|
|
146
|
+
and the user lands on the specific fields this tool is missing.
|
|
147
|
+
|
|
148
|
+
OFFER is deliberately not COLLECT here. Nothing is blocking, so sending the
|
|
149
|
+
user to the edit page would be a detour to answer a question they are
|
|
150
|
+
entitled to ignore -- and this is the path taken when the client could not
|
|
151
|
+
intercept the click, which is exactly when a detour is hardest to undo.
|
|
152
|
+
"""
|
|
153
|
+
base_url = app.config.get('PLEXORA_BASE_URL', '')
|
|
154
|
+
outcome, _, _ = _resolve(datasource, tool_name)
|
|
155
|
+
|
|
156
|
+
if outcome == COLLECT:
|
|
157
|
+
return redirect(f"{base_url}/edit_config/{datasource}?needs={tool_name}")
|
|
158
|
+
if outcome == FALLBACK:
|
|
159
|
+
return redirect(f"{base_url}/{datasource}")
|
|
160
|
+
return redirect(f"{base_url}/{datasource}?tool={tool_name}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.route('/<string:datasource>/tools/<string:tool_name>/panel')
|
|
164
|
+
def tool_panel(datasource, tool_name):
|
|
165
|
+
"""Fetched by toolLoader.js the first time a tool is opened mid-session
|
|
166
|
+
(plain viewer already loaded, no navigation) -- mirrors open_tool()'s
|
|
167
|
+
checks above, but returns JSON for client-side injection instead of a
|
|
168
|
+
redirect, so the viewer/OpenSeadragon instance already on the page is never
|
|
169
|
+
torn down.
|
|
170
|
+
|
|
171
|
+
When something is missing this returns `needs` rather than a redirect. That
|
|
172
|
+
is the point of the modal: navigating away to collect a column name would
|
|
173
|
+
rebuild the whole viewer to answer one question.
|
|
174
|
+
|
|
175
|
+
OFFER is served the same way. The form it produces has no empty blocking
|
|
176
|
+
field and its Cancel is a real answer, so a user who wants none of it is one
|
|
177
|
+
click from the panel -- which is what makes offering it acceptable at all.
|
|
178
|
+
"""
|
|
179
|
+
base_url = app.config.get('PLEXORA_BASE_URL', '')
|
|
180
|
+
outcome, plugin, project = _resolve(datasource, tool_name)
|
|
181
|
+
|
|
182
|
+
if outcome in (COLLECT, OFFER):
|
|
183
|
+
return jsonify({"needs": _needs(plugin, project)})
|
|
184
|
+
if outcome == FALLBACK:
|
|
185
|
+
return jsonify({"redirect": f"{base_url}/{datasource}"}), 400
|
|
186
|
+
|
|
187
|
+
data = template_data(datasource=datasource, active_tool=tool_name)
|
|
188
|
+
fragments = {
|
|
189
|
+
slot: render_template(template_path, data=data)
|
|
190
|
+
for slot, template_path in plugin.panels.items()
|
|
191
|
+
}
|
|
192
|
+
return jsonify({
|
|
193
|
+
"fragments": fragments,
|
|
194
|
+
"scripts": plugin.asset_urls("scripts", base_url),
|
|
195
|
+
"styles": plugin.asset_urls("styles", base_url),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.route('/<string:datasource>/tools/<string:tool_name>/requirements')
|
|
200
|
+
def tool_requirements(datasource, tool_name):
|
|
201
|
+
"""What a tool still needs, without opening it.
|
|
202
|
+
|
|
203
|
+
Lets a plugin ask for something mid-session -- gating wants an image-id
|
|
204
|
+
column only when the user chooses to write gates back to the source file,
|
|
205
|
+
long after the panel opened.
|
|
206
|
+
|
|
207
|
+
`?keys=a,b` adds a `requested` list holding exactly those inputs, if the
|
|
208
|
+
project still cannot answer them. That is a different question from the
|
|
209
|
+
lists above: those are what the user has not been asked yet, this is what a
|
|
210
|
+
specific action needs right now, and an optional field they were offered
|
|
211
|
+
and skipped shows up only in the second.
|
|
212
|
+
"""
|
|
213
|
+
outcome, plugin, project = _resolve(datasource, tool_name)
|
|
214
|
+
if outcome == FALLBACK:
|
|
215
|
+
return jsonify(success=False, error="Unknown tool or datasource"), 404
|
|
216
|
+
|
|
217
|
+
payload = _needs(plugin, project)
|
|
218
|
+
keys = [key for key in (request.args.get("keys") or "").split(",") if key]
|
|
219
|
+
if keys:
|
|
220
|
+
payload["requested"] = [
|
|
221
|
+
r.describe() for r in plugin.requires.requested_from(project, keys)
|
|
222
|
+
]
|
|
223
|
+
return jsonify(success=True, **payload)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@app.route('/<string:datasource>/requirements', methods=['POST'])
|
|
227
|
+
def satisfy_requirements(datasource):
|
|
228
|
+
"""Record answers to whatever was missing.
|
|
229
|
+
|
|
230
|
+
Everything reusable lands on the project, not in the asking plugin's own
|
|
231
|
+
storage: which column holds the cell id is a fact about the dataset, and a
|
|
232
|
+
second plugin needing it must find it already answered. Plugin-private
|
|
233
|
+
state goes through plexora.api.store instead, which is namespaced per
|
|
234
|
+
plugin precisely because it is nobody else's business.
|
|
235
|
+
|
|
236
|
+
Accepts any subset -- the modal posts once with whatever the user filled
|
|
237
|
+
in, and the reply says what is still outstanding.
|
|
238
|
+
"""
|
|
239
|
+
from plexora.server.routes import import_routes
|
|
240
|
+
|
|
241
|
+
project = Project.find(datasource)
|
|
242
|
+
if project is None:
|
|
243
|
+
return jsonify(success=False, error="Unknown datasource"), 404
|
|
244
|
+
|
|
245
|
+
payload = request.get_json(silent=True) or {}
|
|
246
|
+
reload_needed = False
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
if payload.get("data"):
|
|
250
|
+
import_routes.replace_project_data(datasource, payload["data"], payload)
|
|
251
|
+
reload_needed = True
|
|
252
|
+
if payload.get("segmentation"):
|
|
253
|
+
import_routes.attach_segmentation(datasource, payload["segmentation"])
|
|
254
|
+
if not reload_needed:
|
|
255
|
+
# Skipped when the data file itself just changed:
|
|
256
|
+
# replace_project_data already read this answer out of the same
|
|
257
|
+
# payload, and re-applying it would check the choice against the
|
|
258
|
+
# file that has just been replaced.
|
|
259
|
+
from plexora.server.routes.project_routes import apply_feature_choice
|
|
260
|
+
|
|
261
|
+
reload_needed = apply_feature_choice(
|
|
262
|
+
Project.find(datasource) or project, payload)
|
|
263
|
+
answers = ("roles", "columns", "confirm", "coordinates", "single_image",
|
|
264
|
+
"row_number_ids")
|
|
265
|
+
if any(payload.get(key) for key in answers):
|
|
266
|
+
Project.mutate(datasource, lambda p: _apply(p, payload))
|
|
267
|
+
# `single_image` is absent here on purpose: it records what the
|
|
268
|
+
# table already was, so nothing about how it is read has changed.
|
|
269
|
+
# `coordinates` very much has -- it repoints the adapter at a
|
|
270
|
+
# different array -- and so does `row_number_ids`, which clears the
|
|
271
|
+
# obs column the identifier was being read from.
|
|
272
|
+
reload_needed = reload_needed or bool(
|
|
273
|
+
payload.get("roles") or payload.get("columns")
|
|
274
|
+
or payload.get("coordinates") or payload.get("row_number_ids"))
|
|
275
|
+
except ValueError as exc:
|
|
276
|
+
return jsonify(success=False, error=str(exc)), 400
|
|
277
|
+
|
|
278
|
+
if reload_needed:
|
|
279
|
+
error = _reload_or_restore(datasource, project)
|
|
280
|
+
if error:
|
|
281
|
+
return jsonify(success=False, error=error), 400
|
|
282
|
+
|
|
283
|
+
project = Project.find(datasource)
|
|
284
|
+
tool_name = payload.get("tool")
|
|
285
|
+
plugin = plugin_registry.find(app, tool_name) if tool_name else None
|
|
286
|
+
# Everything the tool would still stop for, so the modal knows whether to
|
|
287
|
+
# close or re-render. Unconfirmed items count: naming a data file makes a
|
|
288
|
+
# whole set of column questions askable that were not askable before, and
|
|
289
|
+
# those arrive unconfirmed.
|
|
290
|
+
still_missing = (
|
|
291
|
+
[r.describe() for r in plugin.requires.missing_from(project)]
|
|
292
|
+
+ [r.describe() for r in plugin.requires.unconfirmed_from(project)]
|
|
293
|
+
if plugin else []
|
|
294
|
+
)
|
|
295
|
+
# What is newly offerable, which is not the same list and does not belong in
|
|
296
|
+
# it. Naming a data file makes "which column holds the cell id" ASKABLE for
|
|
297
|
+
# the first time, and for a plugin that only ever offers that question it
|
|
298
|
+
# arrives here rather than in `stillMissing` -- so a modal that closed on
|
|
299
|
+
# `stillMissing` alone would shut the moment the file was attached, one
|
|
300
|
+
# question short. Nothing blocks on these: they keep the form open, they do
|
|
301
|
+
# not keep the tool shut.
|
|
302
|
+
still_optional = (
|
|
303
|
+
[r.describe() for r in plugin.requires.optional_missing_from(project)]
|
|
304
|
+
if plugin else []
|
|
305
|
+
)
|
|
306
|
+
return jsonify(
|
|
307
|
+
success=True,
|
|
308
|
+
stillMissing=still_missing,
|
|
309
|
+
stillOptional=still_optional,
|
|
310
|
+
# Whether the datasource was re-read. The client holds a snapshot of the
|
|
311
|
+
# table's per-column statistics taken at page load, and a tool is drawn
|
|
312
|
+
# from that snapshot -- so an answer that changes which numbers are read
|
|
313
|
+
# (a different matrix, the log transform, a swapped file) leaves the
|
|
314
|
+
# panel showing the old ones unless the client is told to re-fetch.
|
|
315
|
+
reloaded=reload_needed,
|
|
316
|
+
segmentationPending=project.segmentation.pending,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _reload_or_restore(datasource, previous):
|
|
321
|
+
"""Reload the datasource, putting `previous` back if it will not load.
|
|
322
|
+
|
|
323
|
+
An answer is only really accepted once the file can be read with it. A role
|
|
324
|
+
can rewrite the read spec, and some answers are ones the adapter refuses --
|
|
325
|
+
an obs column whose name collides with one it synthesizes, most obviously --
|
|
326
|
+
and it only says so here. Storing such an answer and leaving it stored gives
|
|
327
|
+
the user a project that no longer opens at all, which is a far worse outcome
|
|
328
|
+
than the question they were trying to answer.
|
|
329
|
+
|
|
330
|
+
Scoped to ValueError, which is how an adapter says "I cannot read the file
|
|
331
|
+
this way" and is already what these routes treat as the user's to fix.
|
|
332
|
+
Anything else is a bug rather than a rejected answer, and swallowing it here
|
|
333
|
+
would turn it into a silent no-op save.
|
|
334
|
+
|
|
335
|
+
Returns an error message, or None when the reload was fine.
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
data_model.load_datasource(datasource, reload=True)
|
|
339
|
+
return None
|
|
340
|
+
except ValueError as exc:
|
|
341
|
+
previous.save()
|
|
342
|
+
try:
|
|
343
|
+
data_model.load_datasource(datasource, reload=True)
|
|
344
|
+
except ValueError:
|
|
345
|
+
# The project was already unloadable before this request. Nothing
|
|
346
|
+
# to restore it to, and the message below is still the useful half.
|
|
347
|
+
pass
|
|
348
|
+
return str(exc)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _apply(project, payload):
|
|
352
|
+
columns = payload.get("columns") or {}
|
|
353
|
+
if columns:
|
|
354
|
+
project = project.with_columns(columns.get("markers") or [],
|
|
355
|
+
columns.get("metadata") or [])
|
|
356
|
+
project = apply_column_answers(project, payload)
|
|
357
|
+
# Confirmed last, from two sources. `confirm` is the list of keys the form
|
|
358
|
+
# actually showed: an optional field left blank still counts, because the
|
|
359
|
+
# user was asked and declined, and re-asking on every open is what this
|
|
360
|
+
# list exists to prevent. `_supplied_keys` covers whatever arrived in the
|
|
361
|
+
# payload regardless, so a caller that answers without saying which
|
|
362
|
+
# requirement it was answering does not leave the question open.
|
|
363
|
+
return project.with_confirmed(
|
|
364
|
+
list(payload.get("confirm") or ()) + _supplied_keys(payload))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def apply_column_answers(project, payload):
|
|
368
|
+
"""Record the answers a form gives about a project's columns.
|
|
369
|
+
|
|
370
|
+
Shared by the requirements modal and the project edit page so the two
|
|
371
|
+
cannot drift: both post `roles`, `coordinates`, `single_image` and
|
|
372
|
+
`row_number_ids`, and both have to mean the same thing by them.
|
|
373
|
+
|
|
374
|
+
Order matters. The two flags are applied after the roles because each is an
|
|
375
|
+
alternative answer to a question a role also answers -- naming an image-id
|
|
376
|
+
column retracts a previous "only one image", naming an id column retracts
|
|
377
|
+
"number the rows" (see Project.with_role_answers) -- so a payload carrying
|
|
378
|
+
both means the user has just switched to the flag.
|
|
379
|
+
"""
|
|
380
|
+
project = project.with_role_answers(payload.get("roles") or {})
|
|
381
|
+
coordinates = payload.get("coordinates")
|
|
382
|
+
if coordinates:
|
|
383
|
+
project = project.with_coordinates(coordinates)
|
|
384
|
+
if payload.get("single_image"):
|
|
385
|
+
project = project.with_single_image(True)
|
|
386
|
+
if payload.get("row_number_ids"):
|
|
387
|
+
project = project.with_row_number_ids(True)
|
|
388
|
+
return project
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _supplied_keys(payload):
|
|
392
|
+
"""The requirement keys this payload carries an answer for."""
|
|
393
|
+
keys = []
|
|
394
|
+
if payload.get("data"):
|
|
395
|
+
keys.append("table")
|
|
396
|
+
if payload.get("segmentation"):
|
|
397
|
+
keys.append("segmentation")
|
|
398
|
+
if payload.get("columns"):
|
|
399
|
+
keys.append("markers")
|
|
400
|
+
if payload.get("features_layer") or "features_log" in payload:
|
|
401
|
+
keys.append("features")
|
|
402
|
+
# Filtered against ROLE_NAMES, the same gate with_role_answers applies to
|
|
403
|
+
# the answers themselves. Without it a payload naming a role core cannot
|
|
404
|
+
# store still marked a key confirmed -- so the answer was dropped and the
|
|
405
|
+
# question was never asked again, which is exactly how a project ended up
|
|
406
|
+
# keeping a cell id nobody chose.
|
|
407
|
+
keys.extend(f"role:{role}" for role, column in (payload.get("roles") or {}).items()
|
|
408
|
+
if column and role in ROLE_NAMES)
|
|
409
|
+
if payload.get("coordinates"):
|
|
410
|
+
keys.append("coordinates")
|
|
411
|
+
if payload.get("single_image"):
|
|
412
|
+
# The other answer to the image-id question, and the one that names no
|
|
413
|
+
# column -- so without this the question would be asked again forever.
|
|
414
|
+
keys.append("role:image_id")
|
|
415
|
+
if payload.get("row_number_ids"):
|
|
416
|
+
# Likewise for the cell id.
|
|
417
|
+
keys.append("role:cell_id")
|
|
418
|
+
return keys
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#%%
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
data = pl.read_csv('./unmicst-WD-76845-097-ij_subtracted_50.csv')
|
|
5
|
+
data.head()
|
|
6
|
+
#%%
|
|
7
|
+
data = data.with_columns(pl.lit(0).alias('HE_r'), pl.lit(0).alias('HE_g'), pl.lit(0).alias('HE_b'))
|
|
8
|
+
data.columns
|
|
9
|
+
#%%
|
|
10
|
+
data.write_csv('./unmicst-WD-76845-097-ij_subtracted_50-jj.csv')
|
|
11
|
+
#%%
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Fast, lossless PNG encoding using a libdeflate-backed compressor instead
|
|
2
|
+
of PIL's stdlib-zlib encoder.
|
|
3
|
+
|
|
4
|
+
PIL's PNG encoder is stdlib-zlib-backed, which is markedly slower than a
|
|
5
|
+
libdeflate-backed compressor for the same compression ratio on this kind of
|
|
6
|
+
data (benchmarked on 1024x1024 tiles: PIL compress_level=9 takes ~207ms for
|
|
7
|
+
~1.17MB of 16-bit grayscale channel data that imagecodecs' zlib-ng backed
|
|
8
|
+
compressor gets in ~25ms; on RGBA segmentation-mask tiles, PIL
|
|
9
|
+
compress_level=0 -- current production -- takes ~20ms for ~4.1MB where this
|
|
10
|
+
module gets ~56KB in ~13ms). PIL has no way to select a different
|
|
11
|
+
compression backend, so this module hand-assembles minimal, spec-valid PNGs
|
|
12
|
+
instead of going through PIL at all.
|
|
13
|
+
|
|
14
|
+
Used for:
|
|
15
|
+
- the "HD" full-precision channel-tile quality mode (encode_gray16_png)
|
|
16
|
+
- segmentation/label tiles (encode_rgba8_png) -- safe to use PNG's own
|
|
17
|
+
fast path here despite segmentation's alpha=0-everywhere data (unlike
|
|
18
|
+
WebP, PNG decode never goes through a premultiplied-alpha compositing
|
|
19
|
+
step; the frontend parses PNG bytes directly via UPNG.js, not a canvas)
|
|
20
|
+
"""
|
|
21
|
+
import struct
|
|
22
|
+
import zlib
|
|
23
|
+
|
|
24
|
+
import imagecodecs
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
28
|
+
_ZLIBNG_LEVEL = 6
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _chunk(chunk_type: bytes, data: bytes) -> bytes:
|
|
32
|
+
return (
|
|
33
|
+
struct.pack(">I", len(data))
|
|
34
|
+
+ chunk_type
|
|
35
|
+
+ data
|
|
36
|
+
+ struct.pack(">I", zlib.crc32(chunk_type + data))
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _encode_png(width: int, height: int, bit_depth: int, color_type: int, row_bytes: np.ndarray,
|
|
41
|
+
compress_level: int = _ZLIBNG_LEVEL) -> bytes:
|
|
42
|
+
"""row_bytes: 2D uint8 array, one PNG scanline (without a filter byte) per row.
|
|
43
|
+
|
|
44
|
+
compress_level=0 emits stored (uncompressed) deflate blocks. That is the
|
|
45
|
+
right choice whenever the decoder is JavaScript: pako's inflate dominates
|
|
46
|
+
client-side decode cost, and a stored stream is copied rather than
|
|
47
|
+
Huffman-decoded. It is still a spec-valid PNG.
|
|
48
|
+
"""
|
|
49
|
+
stride = row_bytes.shape[1]
|
|
50
|
+
filtered = np.empty((height, stride + 1), dtype="u1")
|
|
51
|
+
filtered[:, 0] = 0 # filter type 0 ("None") on every scanline
|
|
52
|
+
filtered[:, 1:] = row_bytes
|
|
53
|
+
|
|
54
|
+
idat_data = imagecodecs.zlibng_encode(filtered.tobytes(), level=compress_level)
|
|
55
|
+
ihdr = struct.pack(">IIBBBBB", width, height, bit_depth, color_type, 0, 0, 0)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
_PNG_SIGNATURE
|
|
59
|
+
+ _chunk(b"IHDR", ihdr)
|
|
60
|
+
+ _chunk(b"IDAT", idat_data)
|
|
61
|
+
+ _chunk(b"IEND", b"")
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def encode_gray16_png(tile: np.ndarray, compress_level: int = _ZLIBNG_LEVEL) -> bytes:
|
|
66
|
+
"""Encode a 2D uint16 array as a minimal 16-bit grayscale PNG (IHDR
|
|
67
|
+
bit depth 16, color type 0), byte-for-byte lossless.
|
|
68
|
+
"""
|
|
69
|
+
if tile.ndim != 2:
|
|
70
|
+
raise ValueError(f"expected a 2D array, got shape {tile.shape}")
|
|
71
|
+
if tile.dtype != np.uint16:
|
|
72
|
+
tile = tile.astype(np.uint16)
|
|
73
|
+
|
|
74
|
+
height, width = tile.shape
|
|
75
|
+
# PNG samples are big-endian regardless of host byte order.
|
|
76
|
+
be_tile = np.ascontiguousarray(tile).astype(">u2")
|
|
77
|
+
stride = width * 2
|
|
78
|
+
row_bytes = np.frombuffer(be_tile.tobytes(), dtype="u1").reshape(height, stride)
|
|
79
|
+
|
|
80
|
+
return _encode_png(width, height, bit_depth=16, color_type=0, row_bytes=row_bytes,
|
|
81
|
+
compress_level=compress_level)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def encode_rgba8_png(tile: np.ndarray) -> bytes:
|
|
85
|
+
"""Encode a (H, W, 4) uint8 array as a minimal 8-bit RGBA PNG (IHDR bit
|
|
86
|
+
depth 8, color type 6), byte-for-byte lossless. Used for segmentation
|
|
87
|
+
tiles (packed uint32 label IDs, alpha channel unused/zero).
|
|
88
|
+
"""
|
|
89
|
+
if tile.ndim != 3 or tile.shape[2] != 4:
|
|
90
|
+
raise ValueError(f"expected an (H, W, 4) array, got shape {tile.shape}")
|
|
91
|
+
if tile.dtype != np.uint8:
|
|
92
|
+
tile = tile.astype(np.uint8)
|
|
93
|
+
|
|
94
|
+
height, width, _ = tile.shape
|
|
95
|
+
row_bytes = np.ascontiguousarray(tile).reshape(height, width * 4)
|
|
96
|
+
|
|
97
|
+
return _encode_png(width, height, bit_depth=8, color_type=6, row_bytes=row_bytes)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os
|
|
2
|
+
# set Libvips path for windows. If mac, please run `brew install vips`
|
|
3
|
+
if os.name == 'nt':
|
|
4
|
+
vipshome = os.path.dirname(os.path.abspath(__file__)) + r"\..\static\external\vips-dev-8.10\bin"
|
|
5
|
+
os.environ['PATH'] = vipshome + ';' + os.environ['PATH']
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
Image.MAX_IMAGE_PIXELS = 1000000000
|
|
10
|
+
|
|
11
|
+
# def convertTifToPyramid(channel_img, destFile, isLabelImg):
|
|
12
|
+
# img_dim = channel_img.shape[:2]
|
|
13
|
+
# print('img shape', img_dim, channel_img.dtype)
|
|
14
|
+
# channel_img = channel_img.astype('uint32')
|
|
15
|
+
# imgR = ((channel_img >> 16) % 256).astype('uint8')
|
|
16
|
+
# imgG = ((channel_img >> 8) % 256).astype('uint8') # high bits
|
|
17
|
+
# imgB = (channel_img % 256).astype('uint8') # low bits
|
|
18
|
+
# channel_img = cv2.merge((imgB, imgG, imgR))
|
|
19
|
+
#
|
|
20
|
+
# if channel_img is not None:
|
|
21
|
+
# print('Destfile', destFile)
|
|
22
|
+
# cv2.imwrite(destFile + '.png', channel_img) # [cv2.IMWRITE_PNG_COMPRESSION, 9]
|
|
23
|
+
# image = pyvips.Image.new_from_file(destFile + '.png', access='sequential')
|
|
24
|
+
# # Remove any existing directory with the name of the channel
|
|
25
|
+
# if os.path.exists(destFile):
|
|
26
|
+
# shutil.rmtree(destFile)
|
|
27
|
+
# if not isLabelImg:
|
|
28
|
+
# image.dzsave(destFile, tile_size=128, overlap=2, suffix='.png')
|
|
29
|
+
# else:
|
|
30
|
+
# image.dzsave(destFile, tile_size=128, overlap=2, region_shrink='nearest', suffix='.png')
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# def convertOmeTiff(filePath, fileName, isLabelImg=False):
|
|
34
|
+
# channelNames = []
|
|
35
|
+
# file_path = str(Path(filePath) / fileName)
|
|
36
|
+
# ome = tf.imread(file_path, is_ome=True)
|
|
37
|
+
# for channel in range(np.shape(ome)[0]):
|
|
38
|
+
# img = ome[channel, :, :]
|
|
39
|
+
# channelName = re.sub(r'\.ome|\.tiff|\.tif|\.png', '', fileName) + "_" + str(channel)
|
|
40
|
+
# channel_path = str(Path(filePath) / channelName)
|
|
41
|
+
# convertTifToPyramid(img, channel_path, isLabelImg)
|
|
42
|
+
# channelNames.append(channelName)
|
|
43
|
+
# return channelNames
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# def convertChannel(filePath, isLabelImg):
|
|
47
|
+
# channel_img = imread(filePath)
|
|
48
|
+
# channel_img = np.squeeze(channel_img) # Remove any unnecessary single dimensions
|
|
49
|
+
# channelPath = re.sub(r'\.ome|\.tiff|\.tif|\.png', '', filePath)
|
|
50
|
+
# convertTifToPyramid(channel_img, channelPath, isLabelImg)
|