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,598 @@
|
|
|
1
|
+
"""Single-pass conversion of a raster label mask into the tiled, pyramidal
|
|
2
|
+
outline OME-TIFF the viewer draws.
|
|
3
|
+
|
|
4
|
+
This replaces the old three-step pipeline (pyramid_assemble -> pyramid_upgrade
|
|
5
|
+
-> ensure_outline_segmentation), which built a full *filled* pyramid only to
|
|
6
|
+
discard it: nothing ever served the filled file, because the viewer's label
|
|
7
|
+
layer is outlines-only (see imageViewer.js's "Label/segmentation tiles must
|
|
8
|
+
stay transparent outside outlines"). Writing outlines directly collapses three
|
|
9
|
+
passes into one and drops the throwaway intermediate -- on a 34050x5797 uint32
|
|
10
|
+
Orion mask that is ~60s and ~5MB, where step one alone previously cost ~60s and
|
|
11
|
+
1.19GB.
|
|
12
|
+
|
|
13
|
+
Outlines are recomputed from filled labels independently at every pyramid level
|
|
14
|
+
rather than by downsampling an outline image, which is what keeps boundaries
|
|
15
|
+
continuous (and one pixel wide) as the user zooms out. Labels are downsampled
|
|
16
|
+
with nearest-neighbour sampling so integer cell IDs survive exactly, and the
|
|
17
|
+
full-resolution mask is never required in RAM: each output tile is produced by
|
|
18
|
+
one indexed read of the source.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import tempfile
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Callable, Optional
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import tifffile as tf
|
|
30
|
+
import zarr
|
|
31
|
+
|
|
32
|
+
# Suffix for masks this module generates. The old pipeline wrote
|
|
33
|
+
# ".fast-outlines.pyramid.ome.tiff" even when the boundaries were exact;
|
|
34
|
+
# generated files are now named for what they are. Paths already recorded in
|
|
35
|
+
# config.json keep working -- nothing derives this suffix from a stored path.
|
|
36
|
+
OUTLINE_SUFFIX = ".outlines.pyramid.ome.tiff"
|
|
37
|
+
|
|
38
|
+
# The other thing this module can write: a *filled* label pyramid, for the
|
|
39
|
+
# viewer's on-the-fly mode where the shader derives boundaries per fragment
|
|
40
|
+
# instead of reading them out of the file (see u32_rgba_map in frag.glsl).
|
|
41
|
+
# Cheaper to produce than outlines -- no halo reads, no boundary pass -- and it
|
|
42
|
+
# keeps a cell ID on every pixel rather than only on boundaries.
|
|
43
|
+
FILLED_SUFFIX = ".labels.pyramid.ome.tiff"
|
|
44
|
+
|
|
45
|
+
# Stamped into the OME XML of every mask we write, so recognising our own
|
|
46
|
+
# output is an exact metadata check rather than a statistical guess about
|
|
47
|
+
# pixel content (see generated_mask_kind). The two kinds need distinct markers:
|
|
48
|
+
# a filled pyramid must never be mistaken for outlines, since serving it to a
|
|
49
|
+
# viewer that is not outlining paints solid blobs over the image.
|
|
50
|
+
OUTLINE_MARKER = "plexora-outline-mask"
|
|
51
|
+
FILLED_MARKER = "plexora-label-pyramid"
|
|
52
|
+
|
|
53
|
+
# config.json's `segmentationMode`, and this module's `mode=` argument.
|
|
54
|
+
MODE_OUTLINES = "outlines"
|
|
55
|
+
MODE_FILLED = "filled"
|
|
56
|
+
|
|
57
|
+
# What an import produces unless told otherwise. Filled, because measuring the
|
|
58
|
+
# two against each other on a real 47296x64246 mask found no reason to prefer
|
|
59
|
+
# outlines: per tile, end to end (server read + PNG encode + UPNG decode +
|
|
60
|
+
# renderLabelTile), filled came out at 33.9 ms against 34.7 ms for outlines and
|
|
61
|
+
# 41 KB against 57 KB -- filled labels compress better than scattered boundary
|
|
62
|
+
# pixels, which pays back the client-side boundary pass several times over. And
|
|
63
|
+
# a mask that is *already* a tiled pyramid is served untouched in this mode,
|
|
64
|
+
# where outlines would spend a minute-plus deriving a second copy of it.
|
|
65
|
+
#
|
|
66
|
+
# MODE_OUTLINES is still fully supported and exercised by the tests; nothing in
|
|
67
|
+
# the UI selects it any more. Pass mode= explicitly to get it back.
|
|
68
|
+
DEFAULT_MODE = MODE_FILLED
|
|
69
|
+
|
|
70
|
+
# Ceiling on the level-0 bytes we will pull into RAM in a single read. Reading
|
|
71
|
+
# the whole mask at once is dramatically faster than per-tile reads when the
|
|
72
|
+
# source lives on a network mount -- measured 60s vs 179s for the Orion mask on
|
|
73
|
+
# an SMB share, because hundreds of small scattered reads pay the round-trip
|
|
74
|
+
# each time. Above this budget we fall back to streaming tiles, which bounds
|
|
75
|
+
# memory at the cost of that speedup.
|
|
76
|
+
_DEFAULT_FULL_READ_MAX_BYTES = 8 * 1024 ** 3
|
|
77
|
+
|
|
78
|
+
_FINGERPRINT_SCAN_LIMIT = 20000
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _full_read_budget() -> int:
|
|
82
|
+
raw = os.environ.get("PLEXORA_MASK_FULL_READ_MAX_BYTES")
|
|
83
|
+
if not raw:
|
|
84
|
+
return _DEFAULT_FULL_READ_MAX_BYTES
|
|
85
|
+
try:
|
|
86
|
+
return max(0, int(raw))
|
|
87
|
+
except ValueError:
|
|
88
|
+
return _DEFAULT_FULL_READ_MAX_BYTES
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def derived_output_path(segmentation_path, data_directory=None, *,
|
|
92
|
+
mode: str = MODE_OUTLINES) -> Path:
|
|
93
|
+
"""Destination for the mask derived from `segmentation_path`.
|
|
94
|
+
|
|
95
|
+
Generated masks live in the dataset's own directory (alongside its copied
|
|
96
|
+
feature table) so that removing a dataset removes its derived image too,
|
|
97
|
+
and so we never write into the user's -- possibly read-only or
|
|
98
|
+
network-mounted -- source folder.
|
|
99
|
+
|
|
100
|
+
The two modes get distinct names so switching a datasource between them
|
|
101
|
+
neither overwrites nor silently reuses the other kind of file.
|
|
102
|
+
"""
|
|
103
|
+
source_path = Path(segmentation_path)
|
|
104
|
+
target_dir = Path(data_directory) if data_directory else source_path.parent
|
|
105
|
+
stem = source_path.name
|
|
106
|
+
for extension in (".ome.tiff", ".ome.tif", ".tiff", ".tif", ".png", ".zarr"):
|
|
107
|
+
if stem.lower().endswith(extension):
|
|
108
|
+
stem = stem[: -len(extension)]
|
|
109
|
+
break
|
|
110
|
+
suffix = FILLED_SUFFIX if mode == MODE_FILLED else OUTLINE_SUFFIX
|
|
111
|
+
return target_dir / f"{stem}{suffix}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def outline_output_path(segmentation_path, data_directory=None) -> Path:
|
|
115
|
+
"""Destination for the outline mask derived from `segmentation_path`."""
|
|
116
|
+
return derived_output_path(segmentation_path, data_directory, mode=MODE_OUTLINES)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def source_fingerprint(path) -> Optional[str]:
|
|
120
|
+
"""Cheap staleness key for a mask source, used to decide whether an
|
|
121
|
+
already-generated outline file still corresponds to it. Stat-only: never
|
|
122
|
+
reads pixels, so it is safe to call on every datasource load.
|
|
123
|
+
"""
|
|
124
|
+
source_path = Path(path)
|
|
125
|
+
try:
|
|
126
|
+
stat_result = source_path.stat()
|
|
127
|
+
except OSError:
|
|
128
|
+
return None
|
|
129
|
+
if not source_path.is_dir():
|
|
130
|
+
return f"{stat_result.st_size}-{stat_result.st_mtime_ns}"
|
|
131
|
+
# A .zarr mask is a directory tree, so aggregate over its entries instead.
|
|
132
|
+
count = 0
|
|
133
|
+
total_size = 0
|
|
134
|
+
newest = 0
|
|
135
|
+
for root, _, files in os.walk(source_path):
|
|
136
|
+
for name in files:
|
|
137
|
+
try:
|
|
138
|
+
entry = os.stat(os.path.join(root, name))
|
|
139
|
+
except OSError:
|
|
140
|
+
continue
|
|
141
|
+
count += 1
|
|
142
|
+
total_size += entry.st_size
|
|
143
|
+
newest = max(newest, entry.st_mtime_ns)
|
|
144
|
+
if count >= _FINGERPRINT_SCAN_LIMIT:
|
|
145
|
+
# Pathologically large store: fall back to the directory's own
|
|
146
|
+
# stat rather than walking forever.
|
|
147
|
+
return f"dir-{stat_result.st_mtime_ns}"
|
|
148
|
+
return f"{count}-{total_size}-{newest}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _open_level_zero(path):
|
|
152
|
+
"""Return (array, closer) for the highest-resolution plane of `path`.
|
|
153
|
+
|
|
154
|
+
`is_ome=False` matches how the rest of plexora opens masks: OME metadata on
|
|
155
|
+
these files is decorative, and trusting it makes tifffile reinterpret the
|
|
156
|
+
page layout of some third-party masks.
|
|
157
|
+
"""
|
|
158
|
+
if str(path).endswith(".zarr"):
|
|
159
|
+
group = zarr.open(str(path), mode="r")
|
|
160
|
+
if isinstance(group, zarr.Array):
|
|
161
|
+
return group, lambda: None
|
|
162
|
+
return group["0"], lambda: None
|
|
163
|
+
reader = tf.TiffFile(str(path), is_ome=False)
|
|
164
|
+
store = reader.series[0].aszarr(level=0)
|
|
165
|
+
array = zarr.open(store, mode="r")
|
|
166
|
+
|
|
167
|
+
def _close():
|
|
168
|
+
store.close()
|
|
169
|
+
reader.close()
|
|
170
|
+
|
|
171
|
+
return array, _close
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _memmap_plane(path, expected_shape=None):
|
|
175
|
+
"""A memory map of `path`'s full-resolution plane, or None if it can't be
|
|
176
|
+
mapped.
|
|
177
|
+
|
|
178
|
+
Worth reaching for before the Zarr adapter whenever pixels are read in
|
|
179
|
+
windows. Segmentation masks are routinely written uncompressed and
|
|
180
|
+
*untiled*, frequently as a single strip spanning the whole image, and the
|
|
181
|
+
adapter has to decode a whole strip to answer any read inside it. Sampling
|
|
182
|
+
twelve 512x512 windows out of a 12GB single-strip Orion mask costs 59s that
|
|
183
|
+
way and 0.015s through a memory map.
|
|
184
|
+
|
|
185
|
+
Returns None for anything compressed, tiled or multi-plane, where memmap
|
|
186
|
+
either raises or hands back something that isn't the plane asked for --
|
|
187
|
+
hence `expected_shape`, which callers pass from the reader whose geometry
|
|
188
|
+
they actually trust.
|
|
189
|
+
"""
|
|
190
|
+
if str(path).endswith(".zarr"):
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
candidate = tf.memmap(str(path))
|
|
194
|
+
except Exception:
|
|
195
|
+
return None
|
|
196
|
+
shape = tuple(getattr(candidate, "shape", ()))
|
|
197
|
+
if len(shape) != 2:
|
|
198
|
+
return None
|
|
199
|
+
if expected_shape is not None and shape != tuple(expected_shape):
|
|
200
|
+
return None
|
|
201
|
+
return candidate
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def generated_mask_kind(path) -> Optional[str]:
|
|
205
|
+
"""MODE_OUTLINES, MODE_FILLED, or None for a mask we did not write.
|
|
206
|
+
|
|
207
|
+
Keyed on the OME marker rather than the filename, so a mask the user has
|
|
208
|
+
since renamed or moved is still recognised as generated output.
|
|
209
|
+
"""
|
|
210
|
+
candidate = Path(path)
|
|
211
|
+
if not candidate.is_file():
|
|
212
|
+
return None
|
|
213
|
+
try:
|
|
214
|
+
with tf.TiffFile(str(candidate)) as reader:
|
|
215
|
+
recorded = reader.ome_metadata or ""
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
if OUTLINE_MARKER in recorded:
|
|
219
|
+
return MODE_OUTLINES
|
|
220
|
+
if FILLED_MARKER in recorded:
|
|
221
|
+
return MODE_FILLED
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def is_generated_outline_mask(path) -> bool:
|
|
226
|
+
"""True when `path` is an outline mask this module wrote."""
|
|
227
|
+
return generated_mask_kind(path) == MODE_OUTLINES
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def label_pyramid_gaps(path) -> Optional[list]:
|
|
231
|
+
"""Requirements `path` fails for being served straight to the tile route as
|
|
232
|
+
a filled label layer. An empty list means it can be served untouched; None
|
|
233
|
+
means it could not be read at all.
|
|
234
|
+
|
|
235
|
+
Both requirements matter, and "the mask is already filled labels" satisfies
|
|
236
|
+
neither on its own -- which is the usual surprise. A single-level mask has
|
|
237
|
+
nothing to answer a request for level 3 with (`_zarr_level` hands back full
|
|
238
|
+
resolution for every level, so the viewer draws the wrong scale), and an
|
|
239
|
+
untiled mask makes every tile request decode whole strips -- masks are
|
|
240
|
+
routinely written as one strip spanning the entire image.
|
|
241
|
+
"""
|
|
242
|
+
candidate = Path(path)
|
|
243
|
+
try:
|
|
244
|
+
if str(path).endswith(".zarr"):
|
|
245
|
+
group = zarr.open(str(path), mode="r")
|
|
246
|
+
if isinstance(group, zarr.Array) or len(group) <= 1:
|
|
247
|
+
return ["it has only one resolution level (no pyramid)"]
|
|
248
|
+
return []
|
|
249
|
+
if not candidate.is_file():
|
|
250
|
+
return None
|
|
251
|
+
with tf.TiffFile(str(candidate), is_ome=False) as reader:
|
|
252
|
+
series = reader.series[0]
|
|
253
|
+
page = series.levels[0].pages[0]
|
|
254
|
+
gaps = []
|
|
255
|
+
if len(series.levels) <= 1:
|
|
256
|
+
gaps.append("it has only one resolution level (no pyramid)")
|
|
257
|
+
if not page.is_tiled:
|
|
258
|
+
gaps.append("it is not tiled")
|
|
259
|
+
return gaps
|
|
260
|
+
except Exception:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def is_servable_label_pyramid(path) -> bool:
|
|
265
|
+
"""True when `path` can be served straight to the tile route as a filled
|
|
266
|
+
label layer: tiled, and pyramidal enough for the zoomed-out levels."""
|
|
267
|
+
return label_pyramid_gaps(path) == []
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def looks_like_outline_mask(path) -> bool:
|
|
271
|
+
"""Heuristic "is this mask already outlines?" test for *user-supplied*
|
|
272
|
+
files, where there is no metadata marker to trust.
|
|
273
|
+
|
|
274
|
+
Sampled from contiguous windows on purpose. An earlier version strided the
|
|
275
|
+
whole plane (`level[::step, ::step]`), which for a large mask meant a step
|
|
276
|
+
of ~23px -- far enough apart that neighbouring samples almost never shared
|
|
277
|
+
a label, so every big filled mask scored as "outlines" and outline
|
|
278
|
+
generation was skipped entirely. Interior-ness is a local property and only
|
|
279
|
+
survives contiguous sampling.
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
array, close = _open_level_zero(path)
|
|
283
|
+
except Exception:
|
|
284
|
+
return False
|
|
285
|
+
try:
|
|
286
|
+
if len(array.shape) != 2 or array.size == 0:
|
|
287
|
+
return False
|
|
288
|
+
height, width = (int(size) for size in array.shape)
|
|
289
|
+
# Geometry stays authoritative from the reader opened above (the one
|
|
290
|
+
# that serves tiles); the memory map, when there is one, only makes the
|
|
291
|
+
# windowed reads below cheap enough to run on a datasource load.
|
|
292
|
+
mapped = _memmap_plane(path, (height, width))
|
|
293
|
+
if mapped is not None:
|
|
294
|
+
array = mapped
|
|
295
|
+
window = 512
|
|
296
|
+
nonzero_total = 0
|
|
297
|
+
pixel_total = 0
|
|
298
|
+
interior_total = 0
|
|
299
|
+
boundary_total = 0
|
|
300
|
+
for y_fraction in (0.3, 0.5, 0.7):
|
|
301
|
+
for x_fraction in (0.2, 0.4, 0.6, 0.8):
|
|
302
|
+
y = min(max(0, int(height * y_fraction) - window // 2), max(0, height - window))
|
|
303
|
+
x = min(max(0, int(width * x_fraction) - window // 2), max(0, width - window))
|
|
304
|
+
sample = np.asarray(array[y:y + window, x:x + window])
|
|
305
|
+
if sample.ndim != 2 or sample.size == 0:
|
|
306
|
+
continue
|
|
307
|
+
nonzero = sample != 0
|
|
308
|
+
nonzero_total += int(np.count_nonzero(nonzero))
|
|
309
|
+
pixel_total += sample.size
|
|
310
|
+
center = nonzero[1:-1, 1:-1]
|
|
311
|
+
if center.size == 0:
|
|
312
|
+
continue
|
|
313
|
+
center_values = sample[1:-1, 1:-1]
|
|
314
|
+
same_id_interior = (
|
|
315
|
+
center
|
|
316
|
+
& (center_values == sample[:-2, 1:-1])
|
|
317
|
+
& (center_values == sample[2:, 1:-1])
|
|
318
|
+
& (center_values == sample[1:-1, :-2])
|
|
319
|
+
& (center_values == sample[1:-1, 2:])
|
|
320
|
+
)
|
|
321
|
+
interior_total += int(np.count_nonzero(same_id_interior))
|
|
322
|
+
boundary_total += int(np.count_nonzero(center))
|
|
323
|
+
finally:
|
|
324
|
+
close()
|
|
325
|
+
|
|
326
|
+
if pixel_total == 0 or nonzero_total == 0 or boundary_total == 0:
|
|
327
|
+
return False
|
|
328
|
+
density = nonzero_total / pixel_total
|
|
329
|
+
interior_fraction = interior_total / boundary_total
|
|
330
|
+
return density <= 0.20 and interior_fraction <= 0.05
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def pyramidize_segmentation_mask(
|
|
334
|
+
input_path,
|
|
335
|
+
output_path=None,
|
|
336
|
+
*,
|
|
337
|
+
tile_size: int = 1024,
|
|
338
|
+
compression: Optional[str] = "zlib",
|
|
339
|
+
max_workers: Optional[int] = None,
|
|
340
|
+
outline: bool = True,
|
|
341
|
+
method: str = "exact",
|
|
342
|
+
overwrite: bool = False,
|
|
343
|
+
full_read: Optional[bool] = None,
|
|
344
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
345
|
+
) -> str:
|
|
346
|
+
"""Convert a 2-D label TIFF/Zarr to a tiled, pyramidal OME-TIFF.
|
|
347
|
+
|
|
348
|
+
Parameters
|
|
349
|
+
----------
|
|
350
|
+
input_path
|
|
351
|
+
A single-plane 2-D integer label mask with zero as background. Already
|
|
352
|
+
pyramidal input is fine -- its highest-resolution level is used and a
|
|
353
|
+
fresh outline pyramid is derived from it.
|
|
354
|
+
output_path
|
|
355
|
+
Destination. Defaults to `outline_output_path(input_path)`.
|
|
356
|
+
tile_size
|
|
357
|
+
Square TIFF tile edge, a multiple of 16. Larger tiles cut conversion
|
|
358
|
+
time noticeably on huge masks.
|
|
359
|
+
compression
|
|
360
|
+
Lossless TIFF compression; None writes uncompressed tiles.
|
|
361
|
+
max_workers
|
|
362
|
+
Worker threads tifffile may use for compression.
|
|
363
|
+
outline
|
|
364
|
+
Write only label-boundary pixels, keeping their original cell IDs.
|
|
365
|
+
False writes a conventional filled label pyramid.
|
|
366
|
+
method
|
|
367
|
+
"exact" marks every cell-cell and cell-background boundary
|
|
368
|
+
(8-neighbour). "fast" marks only foreground-background boundaries
|
|
369
|
+
(4-neighbour) -- cheaper, but misses seams where two cells touch.
|
|
370
|
+
overwrite
|
|
371
|
+
Replace an existing destination.
|
|
372
|
+
full_read
|
|
373
|
+
True bulk-reads the whole mask into RAM before tiling, False always
|
|
374
|
+
streams per tile. None (default) decides from the level-0 byte size
|
|
375
|
+
against `PLEXORA_MASK_FULL_READ_MAX_BYTES`.
|
|
376
|
+
progress_callback
|
|
377
|
+
Called as (tiles_written, tiles_total) as the pyramid is written.
|
|
378
|
+
"""
|
|
379
|
+
source_path = Path(input_path).expanduser()
|
|
380
|
+
if not source_path.exists():
|
|
381
|
+
raise FileNotFoundError(source_path)
|
|
382
|
+
if not isinstance(tile_size, int) or tile_size < 16 or tile_size % 16:
|
|
383
|
+
raise ValueError("tile_size must be an integer multiple of 16.")
|
|
384
|
+
outline_method = str(method).strip().lower()
|
|
385
|
+
if outline_method not in {"exact", "fast"}:
|
|
386
|
+
raise ValueError("method must be 'exact' or 'fast'.")
|
|
387
|
+
|
|
388
|
+
destination = (
|
|
389
|
+
outline_output_path(source_path)
|
|
390
|
+
if output_path is None
|
|
391
|
+
else Path(output_path).expanduser()
|
|
392
|
+
)
|
|
393
|
+
if destination.resolve() == source_path.resolve():
|
|
394
|
+
raise ValueError("output_path must be different from input_path.")
|
|
395
|
+
if destination.exists() and not overwrite:
|
|
396
|
+
raise FileExistsError(
|
|
397
|
+
f"Destination already exists: {destination}. Pass overwrite=True to replace it."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
labels = None
|
|
401
|
+
close_source = None
|
|
402
|
+
memmap = None
|
|
403
|
+
temporary_path: Optional[Path] = None
|
|
404
|
+
try:
|
|
405
|
+
# Metadata-only probe first: shape and dtype decide the read strategy,
|
|
406
|
+
# so they must be known before anything is pulled into memory.
|
|
407
|
+
if str(source_path).endswith(".zarr"):
|
|
408
|
+
probe, probe_close = _open_level_zero(source_path)
|
|
409
|
+
shape, dtype = probe.shape, np.dtype(probe.dtype)
|
|
410
|
+
probe_close()
|
|
411
|
+
else:
|
|
412
|
+
with tf.TiffFile(str(source_path), is_ome=False) as reader:
|
|
413
|
+
series = reader.series[0]
|
|
414
|
+
shape, dtype = series.shape, np.dtype(series.dtype)
|
|
415
|
+
if len(shape) != 2:
|
|
416
|
+
raise ValueError(
|
|
417
|
+
"input_path must contain one 2-D label plane; "
|
|
418
|
+
f"found shape {tuple(shape)!r}."
|
|
419
|
+
)
|
|
420
|
+
if not np.issubdtype(dtype, np.integer):
|
|
421
|
+
raise TypeError(f"Label mask must have an integer dtype, found {dtype}.")
|
|
422
|
+
height, width = (int(size) for size in shape)
|
|
423
|
+
if height == 0 or width == 0:
|
|
424
|
+
raise ValueError("Label mask must have non-zero height and width.")
|
|
425
|
+
|
|
426
|
+
use_full_read = (
|
|
427
|
+
bool(full_read)
|
|
428
|
+
if full_read is not None
|
|
429
|
+
else (height * width * dtype.itemsize) <= _full_read_budget()
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if use_full_read:
|
|
433
|
+
# One sequential read, then every tile is served from RAM. Routed
|
|
434
|
+
# through `memmap` so the direct-indexing branches below (which
|
|
435
|
+
# already special-case a real ndarray) pick it up unchanged.
|
|
436
|
+
if str(source_path).endswith(".zarr"):
|
|
437
|
+
array, close_source = _open_level_zero(source_path)
|
|
438
|
+
memmap = np.asarray(array)
|
|
439
|
+
close_source()
|
|
440
|
+
close_source = None
|
|
441
|
+
else:
|
|
442
|
+
memmap = np.asarray(tf.imread(str(source_path), is_ome=False, series=0, level=0))
|
|
443
|
+
labels = memmap
|
|
444
|
+
else:
|
|
445
|
+
# Streaming per tile: map the source where possible, so each tile is
|
|
446
|
+
# a handful of page faults rather than a strip decode.
|
|
447
|
+
memmap = _memmap_plane(source_path, (height, width))
|
|
448
|
+
if memmap is not None:
|
|
449
|
+
labels = memmap
|
|
450
|
+
if labels is None:
|
|
451
|
+
labels, close_source = _open_level_zero(source_path)
|
|
452
|
+
|
|
453
|
+
factors = [1]
|
|
454
|
+
while max(
|
|
455
|
+
(height + factors[-1] - 1) // factors[-1],
|
|
456
|
+
(width + factors[-1] - 1) // factors[-1],
|
|
457
|
+
) > tile_size:
|
|
458
|
+
factors.append(factors[-1] * 2)
|
|
459
|
+
total_tiles = sum(
|
|
460
|
+
(((height + factor - 1) // factor + tile_size - 1) // tile_size)
|
|
461
|
+
* (((width + factor - 1) // factor + tile_size - 1) // tile_size)
|
|
462
|
+
for factor in factors
|
|
463
|
+
)
|
|
464
|
+
tiles_written = 0
|
|
465
|
+
|
|
466
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
467
|
+
file_descriptor, temporary_name = tempfile.mkstemp(
|
|
468
|
+
dir=destination.parent,
|
|
469
|
+
prefix=f".{destination.name}-",
|
|
470
|
+
suffix=".tmp.ome.tiff",
|
|
471
|
+
)
|
|
472
|
+
os.close(file_descriptor)
|
|
473
|
+
temporary_path = Path(temporary_name)
|
|
474
|
+
|
|
475
|
+
def outline_tile(halo: np.ndarray) -> np.ndarray:
|
|
476
|
+
center = halo[1:-1, 1:-1]
|
|
477
|
+
if outline_method == "fast":
|
|
478
|
+
foreground = center != 0
|
|
479
|
+
is_boundary = foreground & (
|
|
480
|
+
(halo[:-2, 1:-1] == 0) | (halo[2:, 1:-1] == 0)
|
|
481
|
+
| (halo[1:-1, :-2] == 0) | (halo[1:-1, 2:] == 0)
|
|
482
|
+
)
|
|
483
|
+
else:
|
|
484
|
+
# An exact boundary is any labelled pixel adjacent to a
|
|
485
|
+
# *different* label or to background -- not just those next to
|
|
486
|
+
# empty space, which is what makes touching cells separable.
|
|
487
|
+
is_boundary = (center != 0) & (
|
|
488
|
+
(halo[:-2, 1:-1] != center) | (halo[2:, 1:-1] != center)
|
|
489
|
+
| (halo[1:-1, :-2] != center) | (halo[1:-1, 2:] != center)
|
|
490
|
+
| (halo[:-2, :-2] != center) | (halo[:-2, 2:] != center)
|
|
491
|
+
| (halo[2:, :-2] != center) | (halo[2:, 2:] != center)
|
|
492
|
+
)
|
|
493
|
+
return np.where(is_boundary, center, 0)
|
|
494
|
+
|
|
495
|
+
def read_block(y_start, y_stop, x_start, x_stop, factor):
|
|
496
|
+
if factor == 1:
|
|
497
|
+
return np.asarray(labels[y_start:y_stop, x_start:x_stop])
|
|
498
|
+
rows = np.arange(y_start * factor, y_stop * factor, factor)
|
|
499
|
+
columns = np.arange(x_start * factor, x_stop * factor, factor)
|
|
500
|
+
if memmap is not None:
|
|
501
|
+
return np.asarray(labels[np.ix_(rows, columns)])
|
|
502
|
+
return np.asarray(labels.oindex[rows, columns])
|
|
503
|
+
|
|
504
|
+
def tiles_for_level(factor: int):
|
|
505
|
+
nonlocal tiles_written
|
|
506
|
+
level_height = (height + factor - 1) // factor
|
|
507
|
+
level_width = (width + factor - 1) // factor
|
|
508
|
+
for y_start in range(0, level_height, tile_size):
|
|
509
|
+
y_stop = min(y_start + tile_size, level_height)
|
|
510
|
+
for x_start in range(0, level_width, tile_size):
|
|
511
|
+
x_stop = min(x_start + tile_size, level_width)
|
|
512
|
+
if outline:
|
|
513
|
+
# Read a one-pixel halo *in this level's* coordinates.
|
|
514
|
+
# Comparing against real neighbours rather than a
|
|
515
|
+
# zero-padded edge is what keeps an outline unbroken
|
|
516
|
+
# where two TIFF tiles meet.
|
|
517
|
+
read_y_start = max(0, y_start - 1)
|
|
518
|
+
read_y_stop = min(level_height, y_stop + 1)
|
|
519
|
+
read_x_start = max(0, x_start - 1)
|
|
520
|
+
read_x_stop = min(level_width, x_stop + 1)
|
|
521
|
+
sampled = read_block(
|
|
522
|
+
read_y_start, read_y_stop, read_x_start, read_x_stop, factor
|
|
523
|
+
)
|
|
524
|
+
halo = np.zeros(
|
|
525
|
+
(y_stop - y_start + 2, x_stop - x_start + 2), dtype=dtype
|
|
526
|
+
)
|
|
527
|
+
insert_y = read_y_start - (y_start - 1)
|
|
528
|
+
insert_x = read_x_start - (x_start - 1)
|
|
529
|
+
halo[
|
|
530
|
+
insert_y:insert_y + sampled.shape[0],
|
|
531
|
+
insert_x:insert_x + sampled.shape[1],
|
|
532
|
+
] = sampled
|
|
533
|
+
tile = outline_tile(halo)
|
|
534
|
+
else:
|
|
535
|
+
tile = read_block(y_start, y_stop, x_start, x_stop, factor)
|
|
536
|
+
if tile.shape != (tile_size, tile_size):
|
|
537
|
+
padded = np.zeros((tile_size, tile_size), dtype=dtype)
|
|
538
|
+
padded[: tile.shape[0], : tile.shape[1]] = tile
|
|
539
|
+
tile = padded
|
|
540
|
+
tiles_written += 1
|
|
541
|
+
if progress_callback is not None:
|
|
542
|
+
progress_callback(tiles_written, total_tiles)
|
|
543
|
+
yield np.ascontiguousarray(tile)
|
|
544
|
+
|
|
545
|
+
with tf.TiffWriter(str(temporary_path), bigtiff=True, ome=True) as writer:
|
|
546
|
+
for level_index, factor in enumerate(factors):
|
|
547
|
+
level_shape = (
|
|
548
|
+
(height + factor - 1) // factor,
|
|
549
|
+
(width + factor - 1) // factor,
|
|
550
|
+
)
|
|
551
|
+
metadata = None
|
|
552
|
+
if level_index == 0:
|
|
553
|
+
metadata = {"axes": "YX", "Channel": {"Name": "cell"}}
|
|
554
|
+
metadata["Name"] = OUTLINE_MARKER if outline else FILLED_MARKER
|
|
555
|
+
writer.write(
|
|
556
|
+
tiles_for_level(factor),
|
|
557
|
+
shape=level_shape,
|
|
558
|
+
dtype=dtype,
|
|
559
|
+
tile=(tile_size, tile_size),
|
|
560
|
+
compression=compression,
|
|
561
|
+
photometric="minisblack",
|
|
562
|
+
metadata=metadata,
|
|
563
|
+
subifds=len(factors) - 1 if level_index == 0 else None,
|
|
564
|
+
subfiletype=1 if level_index else None,
|
|
565
|
+
maxworkers=max_workers,
|
|
566
|
+
)
|
|
567
|
+
os.replace(temporary_path, destination)
|
|
568
|
+
temporary_path = None
|
|
569
|
+
finally:
|
|
570
|
+
if memmap is not None:
|
|
571
|
+
underlying = getattr(memmap, "_mmap", None)
|
|
572
|
+
if underlying is not None:
|
|
573
|
+
underlying.close()
|
|
574
|
+
if close_source is not None:
|
|
575
|
+
close_source()
|
|
576
|
+
if temporary_path is not None:
|
|
577
|
+
temporary_path.unlink(missing_ok=True)
|
|
578
|
+
|
|
579
|
+
return str(destination)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
__all__ = [
|
|
583
|
+
"FILLED_MARKER",
|
|
584
|
+
"FILLED_SUFFIX",
|
|
585
|
+
"MODE_FILLED",
|
|
586
|
+
"MODE_OUTLINES",
|
|
587
|
+
"OUTLINE_MARKER",
|
|
588
|
+
"OUTLINE_SUFFIX",
|
|
589
|
+
"derived_output_path",
|
|
590
|
+
"generated_mask_kind",
|
|
591
|
+
"is_generated_outline_mask",
|
|
592
|
+
"is_servable_label_pyramid",
|
|
593
|
+
"label_pyramid_gaps",
|
|
594
|
+
"looks_like_outline_mask",
|
|
595
|
+
"outline_output_path",
|
|
596
|
+
"pyramidize_segmentation_mask",
|
|
597
|
+
"source_fingerprint",
|
|
598
|
+
]
|