diff --git a/CHANGELOG.md b/CHANGELOG.md index a421218c..5cc49252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,10 +73,14 @@ All notable changes to cellmap-flow. The format follows [Keep a Changelog](https - **Shift+R restarts finetuning from the viewer.** Paint, then press Shift+R over Neuroglancer (or over the dashboard, outside a text field): the job waiting for a restart trains again with the Finetune tab's settings as they are, as *Restart Training* does but without its confirm, and the key says why not when there is nothing to restart. The Restart button's tooltip and the Training panel say so. See `docs/finetuning.md` and the docs' *In the browser* page; *A finetune job* in the docs' architecture page says how the press reaches the tab. ### Changed +- **The browser trains on the server's own training targets.** The binary, broadcast, affinity, distance and interval targets are numpy functions in `cellmap_flow/portable/targets.py`, which `finetune/target_transforms.py`'s classes now wrap (same classes, same results) and the browser's head trainer, whole-network trainer and ranking call in the page's Python (`portable/targets_calls.py`, from `web/src/finetune/target-call.ts`). The affinity target takes its offsets as given, so an offset means the same in both modes however it was found: the server from the model's script or its `_aff` channels as before, the browser from its manifest's channel names (no manifest on Hugging Face names offsets). An offset as long as the patch or longer now pairs nothing in both modes, where the server raised for one between one and two patch lengths. The TypeScript port (`web/src/finetune/targets.ts`), its parity fixture (`targets.json`, the `targets` section of `make_fixtures.py`) and parity test are gone. +- **Label it's seed is written once, in Python, and the browser runs it too.** `cellmap_flow/portable/label_it.py` holds what *Seed from Prediction* makes of a model's output: the request's options, the channels a seed reads, which served voxel lies under each annotation voxel, the output as a probability and the *components* method (threshold, connected components, smallest size, per slice), out of `dashboard/routes/finetune/view_labels.py` (which keeps the model server's read and the paint's write around it) and `post/segment.py` (`connected_components` and its options, imported from there now, as `serving/probe.py` imports the activation classes). The browser calls it through `portable/label_it_calls.py` in the engine's Pyodide worker, and its TypeScript twin is gone: `web/src/label-it/segment.ts` (241 → 52 lines) and `prediction.ts` (116 → 80) keep the region read and the mutex watershed, and the parity fixture's probability, components and read_prediction cases went with them; the browser's ranking reads its placement and probability from the same Python. The mutex watershed stays per mode, as `mwatershed` has no Pyodide wheel: the Python hands the browser's (`web/src/infer/affinity.ts`, still parity-tested) its input. scipy, the first the browser's Python uses, loads on the first seed and not when a dataset opens: 15 MB downloaded once per browser, and about 4 s on a page's first seed; a seed of a 64³ box then takes about 30 ms (55 ms in the TypeScript). A seed in the browser now follows the server's rules: an affinity model's channels and offsets are its first `_aff` channels counted (up to three, along z, y and x), where the TypeScript read the `_aff_z`, `_aff_y` and `_aff_x` suffixes wherever they were; a channel range running past the model's reads the channels there are, where it threw; `connectivity: null` is refused (400) where it meant 1; and with no dataset open a seed is refused before anything else. The server's probability takes numpy's sigmoid in place of scipy's `expit`, within one float32 ulp of it, so a mutex-watershed seed and the ranking need no scipy in the browser. +- **A new annotation volume and its good regions are planned once, in Python, for both modes.** `cellmap_flow/portable/volume.py` plans a volume on numbers alone (the raw level the model reads, the model's voxel sizes and blocks) and holds the labels' dtype rule; `portable/regions.py` holds the good regions' rules: the region *Mark as Good* makes and its size, which stored entries are regions, and the block a region anchors the model in. The server's `plan_volume`, `annotation_dtype_for`, good-region routes and rehearsal pool call them in their process, the browser through `portable/volume_calls.py` in its Pyodide worker, so creating a volume or marking a region there needs the Python loaded with the dataset; the TypeScript copies (`planVolume`'s grid and `annotationDtypeFor` in `web/src/finetune/volume.ts`; `regionAt`, `defaultRegionSizeNm`, `isGoodRegion` and the anchors' maths in `good-regions.ts`) are gone with their hand-computed tests. What changes: a volume lies on the grid its model's prediction is served on in both modes, the level the model reads times the model's ratio of output to input voxel size, from that level's corner, where the server snapped the output voxel size to a raw level of its own (on a pyramid of 10, 20 and 40 nm, a model reading 8 nm and writing 16 got a 10 nm volume, its chunks half the model's blocks); the browser rounds a partial output voxel at the far end of the data up, as the server did, where it dropped it; a browser volume's labels are uint16 for an affinity model only, as on the server, no longer for a chain that ends in labels; `size_nm: 0` marks one output block in the browser too, where it was refused, and both modes refuse a size that is a string, not finite, or two numbers (the server took `"40"` and NaN, and answered numpy's error for two); a stored region needs three finite numbers for its corner and three positive ones for its size, on the server too, where a two-number box stopped the trainer. +- **AI-assisted annotation's maths is written once, in Python, and the browser runs it.** `cellmap_flow/portable/ai_annotate.py` holds the plane (`plan_plane`, `raw_box`, the screen layout), the image sent (laid out, resampled, scaled to 8 bits), the mask in the model's reply (resized back to the size sent, the padding cut off, the colour thresholded), its objects on the write box, the review's overlay and the labels Accept writes; `portable/fill.py` holds the box decisions under it and under the Label it tools (`box_voxels`, fresh ids, `seed_labels`, `relabel_objects`, which voxels a write changes and an undo puts back). `ai_annotate/geometry.py`, `pipeline.py`, `mask_extraction.py` and `staging.py`, the Vertex backend and `finetune/session/fill.py` call them around their zarr reads, MinIO writes and PNGs, under the same names and with the same JSON; the browser calls them in chunkmirage's Pyodide worker (`portable/ai_annotate_calls.py`), scipy loading while the model works on the first run. The images are resampled by Pillow itself in both modes: the server's installed Pillow, as before, and in the browser the one Pyodide 0.28.3 ships (11.3.0, a 1 MB wheel), which chunkmirage's worker loads the first time a call imports it, as it loads scipy; the first reply resized waits about 0.3 s for it, once, and a 1024² reply then resizes to 512² in about 9 ms. Gone: the TypeScript ports in `web/src/ai-annotate/geometry.ts`, `plane.ts` (with its port of Pillow's resampling) and `mask.ts`, the codec's canvas resize, and `web/test/fixtures/make_ai_fixtures.py` with `ai_annotate.json` and `web/test/parity/ai-annotate.test.ts`. What changes in the browser: a reply at another size is resized by Pillow's bilinear filter before the threshold, not by the canvas's smoothing, so the mask's edges are the server's (Pyodide's Pillow 11.3 gives the same pixels as 12.3 on the cases tried; 13.0 rounds its 8-bit bilinear one level differently at a non-integer ratio, which the colour threshold cannot tell); the overlay rounds halves to even, as the server's does; and Accept decides by the server's rule (`fill.counts_up`) whether an instance target's ids count up, on any volume of more than 8-bit ids. Label it's box tools run on the same Python: *Seed from Prediction*'s write, *All Background*, *Split Objects* and *Undo* (`web/src/backend/routes/view-labels.ts`) call `portable/fill.py` through `portable/fill_calls.py` around the paint store's reads and writes, so `web/src/ai-annotate/labels.ts` and the maths in `fill.ts` are gone with the fill cases of `make_label_it_fixtures.py` and `label-it.test.ts`; with no dataset open they are refused (409), as a seed is, since the Python runs in the dataset's engine, and the page's first Split waits for scipy to load (a few seconds, once) when no seed has loaded it. `fill.py` counts a box's (object, id) pairs by one key per pair, the same answers in a tenth of the time, so a Split of a 64³ box takes as long in the browser as the TypeScript took (about 30 ms). - **The volume reader is [worldarray](https://github.com/JaneliaSciComp/worldarray).** `ImageDataInterface` reads through a `WorldArray` (new dependency `worldarray>=0.1,<1`): where the data sits in nm, which level of a pyramid to read for a voxel size, padding past the volume and resampling to a voxel size the pyramid lacks are its, written once where cellmap-flow, cellmap-analyze, mesh-n-bone and four more of cellmap's repositories each carried a diverged copy; `cellmap_flow/io/` keeps the chain and channel classes, cellmap-flow's level names, path forms and ROI rule (2,201 → 1,059 lines). Its conventions (`docs/spec.md` in that repo) move some volumes by up to half a voxel, or a rounding step, from where cellmap-flow read them: a COSEM `transform.translate` and a zarr v3 transform attribute name the centre of the first voxel, as an OME `translation` already did; an offset is never rounded to the voxel grid; a group's `multiscales` transform outranks an array's own `resolution`; an N5 level with `downsamplingFactors` and no offset is placed as BigDataViewer places it; the nearest level on a tie is the finer; a resampling block cut short by the end of the data is the mean of what it has. Reading `jrc_hela-2`'s N5 and OME-Zarr copies now gives the same corner. `ImageDataInterface.ts`, `read_box`, the normalizer chain, `on_voxel_size_mismatch` and `model_info`'s `input_resampled_from` are unchanged; `cellmap_flow.io.store.prepare` and `io.source.reader_context` are gone (worldarray's `capped_context` and GCE setting do their work). - **One definition of the training settings, from the form to the trainer.** `cellmap_flow/finetune/options.py`'s `TrainingOptions` holds every setting a finetune trains with and its default, once: the mode (`lora`, `full`, or the browser's `head`, in place of "a LoRA rank of 0 means full"), rank and alpha, epochs, batch, learning rate, loss, margin, smoothing, class balance, distillation weight and scope, augmentation, mixed precision, the target type, channel and offsets, the output class, and the manifest's patches per epoch and rehearsal fraction. The submit and restart bodies, the job's command line (every flag made from the model's fields, the old flag names kept so a running job still takes a restart), the trainer and the browser's `FINETUNE_DEFAULTS` are all read from it, where seven layers each listed the settings with defaults that had drifted (the form's margin loss and batch of 8 against the trainer's combined loss and the request's batch of 2); the CLI's defaults are the form's now. `TrainingOptions.for_session` is what submit adjusts for a painted session or a distance or flow target, and a restart's values are checked as the form's are. The trainer reads the model's output as its server does: `output_class` (`serving.probe`'s unit, signed_unit or unbounded) comes from the running model's `model_info`, or `--output-class`, or one probe in the job, in place of the trainer's own sigmoid probe, which took a tanh's [-1, 1] for logits while ranking and seeding read it as (p + 1) / 2. `lora_trainer.py` (1,516 lines) is split into `finetune/trainer/`: `loop.py` (`Finetuner`; `LoRAFinetuner` stays as its old name), `supervised.py`, `distillation.py`, `recovery.py`, `diagnostics.py` and `tensorboard.py`, the checkpoint files written and read by `adaptation.py`'s strategies; `BatchLoopWrapper` and `SequentialWrapper` sit with the other wrappers in `trainable.py`. - **One way to describe a model, and one way to start its server.** Every description of a model is now a model entry, `{type, name, env, **arguments}` (`models/entry.py`), and `registry.build_model` is the one place a config is built from one: a YAML's `models:`, `cellmap_flow infer `'s options, the pipeline builder's model form, the Models tab's catalog, Hugging Face, zoo and Cellpose picks, *Add a model*'s reference and `cellmap_flow serve --model` all go through it, where `infer`, the form and the server each built the config in their own way. The entry's values become constructor arguments by the constructor's annotations (`entry.coerce`): a string from the command line or the form, a number or a list from a YAML, give the same config, where the command line gave ints and the form floats, and each constructor parsed strings itself. The form posts the type's name (`type: fly`); the class name it posted before (`class_name: FlyModelConfig`) is still taken, with a deprecation warning, until the release after 0.3.0. `serving.launch.launch` starts every server, from the CLIs, the Models tab and the Training tab's ranking alike: it applies the model's `scale` to the data path, falls back to the saved queue and charge group, builds the command and starts the job. The Models tab's five launchers and four launch loops are one path over the configs it built, which also keeps a catalog model's config among the session's models, so Export Config lists it. A zoo model's entry gets its `voxel_size` by the one rule its server applies (the one given, else the description's, else the one it was trained at), the picker reads axis units from `io.metadata`'s table, as the server does, so a model whose axes are in picometres, centimetres or metres is accepted, and a catalog folder that is not there is refused at Submit rather than on the node. The Models tab's script is one module per list (`static/js/dashboard/models/`), each saying what it submits and how to untick a row, so Submit is the merge of the lists and a ✕ tells each one; the page behaves as before. -- **chunkmirage at `7307e36b`, one commit for both modes.** `pyproject.toml` (the inference server) and `web/chunkmirage.lock` (the browser engine) pin the same chunkmirage commit again, its main of 2026-10-21 (`Pipeline.plan`, `materialize` over a box, the core/contrib split); `Cast` is imported from `chunkmirage_contrib.ops`, where that split put it, and `tests/web/test_pins.py` holds the two pins together without its xfail. +- **chunkmirage 0.1.0a2 from PyPI for the server, `a7eb44e6` for the browser.** `pyproject.toml` takes `chunkmirage>=0.1.0a2,<0.2` from PyPI where it named a git commit, as PyPI refuses an upload whose dependencies name a URL: cellmap-flow can be released again. `web/chunkmirage.lock` (the browser engine) pins `a7eb44e6`: the release's commit (`584b343`) and chunkmirage's PR #50 after it, which changed only the browser engine, whose workers now load any package Pyodide ships the first time a page's Python imports it, found by its module in Pyodide's lockfile (`PIL` is Pillow's), where only scipy and scikit-image loaded that way. What they bring over the last pin: `Pipeline.plan`, `materialize` over a box, the core/contrib split (`Cast` is imported from `chunkmirage_contrib.ops`, where it went), stored arrays opened and placed by worldarray, as cellmap-flow's reader opens them, and an array nothing places given its corner at the origin (voxel 0's centre half a voxel on), as Neuroglancer draws one; the server's ops needed no change. `tests/web/test_pins.py` holds the server to a release by version and the browser to a full commit, which is the release's or a later one. - **The constants and catalogs the browser app shares with Python are generated from the Python.** `web/build/export_data.py` writes `web/src/generated/python-data.json` (committed; `npm run export:data`, and `npm run render` with the page), and the app reads it through `web/src/generated/python-data.ts` in place of the copies it kept by hand: the op registry with each op's parameters and whether the browser runs it, the default chain (written once, where `build/render.py` and `src/state/index.ts` each had it), the organelle catalog, the prompt's sentences and the AI-annotate limits and labels, the raw and prediction shaders, the prediction colours and the layer keys tied to a source, the ranking's constants, the finetune jobs' terminal statuses and log patterns, Label it's undo depth and large-box size, and the unit table. The prompt's sentences live in `ai_annotate/prompts.py SENTENCES`, and the registries' listing is one function, `input_normalize.op_listing`. `tests/web/test_python_data_fresh.py` fails when the file is stale, and replaces `tests/web/test_layer_carry.py`'s scrape of `viewer/state.ts`. - **Past the edge of the volume, the models see its outermost voxels repeated, not a flat fill.** Near any face of the volume a model's input runs past the data, by its context (44 voxels each way for `setup_56`). That part is now the data's edge repeated outward, after the input normalizers, in the browser and on the server alike: serving, blockwise, and finetuning (the browser's last layer and whole network, the server's trainer), so a finetune trains on what it is served with. It was 0 after the normalizers when serving (mid-grey on the manifests' −1 to 1 scale), and −1 (black) in the server's training patches, which were normalized after padding. On jrc_jurkat-1, whose 128 nm level is 93 voxels thick, `setup_56` found 60% of each nucleus the volume's edge cuts, where it found 28% (Dice against OpenOrganelle's `nucleus_seg`, 0.62 → 0.78), and called 0.5% of the background nucleus, where it called 0.1%. `ImageDataInterface` gains `padding="zero" | "edge"`; labels are still read with zeros. - **A finetune's restart in the browser replaces the job's previous model.** Each Restart made a new model (`-ft2`, `-ft3`, ...) and kept every earlier one in the Models tab and in the browser's storage, 3.9 to 43 MB each; now a job keeps its latest model only, as the server serves only a job's latest iteration. See *Finetuning in the browser* in the docs' *In the browser* page. @@ -110,7 +114,7 @@ All notable changes to cellmap-flow. The format follows [Keep a Changelog](https - **The Flask inference server** (`cellmap_flow/server.py`, `CellMapFlowServer`) and `CELLMAP_FLOW_ENGINE`: chunkmirage serves every model. A model's own environment (`env:`) must have chunkmirage installed, as every environment in `pixi.toml` has; a server started where it is missing fails saying so, where it fell back to the Flask server with a warning. The dashboard draws only chunkmirage layers (`zarr:////zarr`, channels first), so this dashboard no longer draws a server from an older cellmap-flow, nor an older dashboard this release's servers. With the server go `cellmap_flow serve --debug` (Flask's dev-server flag, which the chunkmirage server ignored), `CELLMAP_FLOW_RAW_READ_CONCURRENCY` (`CELLMAP_FLOW_RAW_CACHE_BYTES` stays), the request-cancellation plumbing in `inference.runner` (chunkmirage drops the chunks nobody waits for itself), `ImageDataInterface`'s `concurrency_limit` and `cache_bytes` arguments (the server was their only user; `io.source.open_array` keeps them) and the `flask-cors` dependency. Flask itself stays: the dashboard is a Flask app. ### Fixed -- **A `cellmap` model is served at the shape its export runs at.** cellmap_models traces the TorchScript at the metadata's `inference_input_shape`, and a traced UNet runs only at that shape; the model type read the training `input_shape` instead, so `jrc_mus-livers_16nm_to_8nm_mito` and `_peroxisome` (216³ in training, 288³ exported) failed every chunk with "padded input size per channel: (1 x 1 x 1)". The inference shapes are used when the metadata has them, as the Pipeline builder already showed them; the training shapes otherwise. +- **A `cellmap` model is served at the shape its export runs at.** cellmap_models traces the TorchScript at the metadata's `inference_input_shape`, and a traced UNet runs only at that shape; the model type read the training `input_shape` instead, so `jrc_mus-livers_16nm_to_8nm_mito` and `_peroxisome` (216³ in training, 288³ exported) failed every chunk with "padded input size per channel: (1 x 1 x 1)". The export is traced at whichever shape the exporter passed, and the metadata does not say which (`mito_aff_unet_setup_16` is the other way round: traced at its training 178³, it answers 56³ to the inference 378³), so the model type now asks the model: a forward at the training shape that answers the training output means the training shapes, anything else the inference shapes. - **daisy is pinned below 2.** daisy 2.0.0 (released 2026-10-21) is a rewrite without `daisy.tcp`, `daisy.Server` and `daisy.Client`, which blockwise uses; the pin keeps installs on the 1.2 line until blockwise is ported. - **A model's `scale` is honoured wherever it is started.** A config with `scale` launched from the Models tab (ticked again after a YAML started it, or run by *Add a model* or *Use this model*) was served at the session's data path, the scale ignored; only `cellmap_flow infer`, `yaml` and blockwise applied it. Every launcher now reads the level the scale names (`serving.launch.launch`). - *Use this model* on the server failed carrying a uint8 volume's paint onto a uint16 one (an affinity model's): numpy 2 refuses to cap uint8 labels at 65535. The cap applies only where the old dtype can exceed the new one. diff --git a/cellmap_flow/ai_annotate/backends/vertex_gemini.py b/cellmap_flow/ai_annotate/backends/vertex_gemini.py index d222dd80..ca6671dd 100644 --- a/cellmap_flow/ai_annotate/backends/vertex_gemini.py +++ b/cellmap_flow/ai_annotate/backends/vertex_gemini.py @@ -33,7 +33,7 @@ from cellmap_flow.ai_annotate.backends.base import SegmentRequest, SegmentResult from cellmap_flow.ai_annotate.errors import INSTALL_HINT, AIAnnotateError from cellmap_flow.ai_annotate.images import decode_model_image -from cellmap_flow.ai_annotate.mask_extraction import extract_mask +from cellmap_flow.portable.ai_annotate import model_mask logger = logging.getLogger(__name__) @@ -128,11 +128,11 @@ def segment(self, request: SegmentRequest, model: str) -> SegmentResult: model_image = decode_model_image(image_bytes) if model_image.size != sent.size: _warn_if_aspect_differs(model_image.size, sent.size) - model_image = model_image.resize(sent.size, Image.BILINEAR) - # Drop the padding _square added: what is left lines up with the plane. - model_image = model_image.crop((0, 0, *request.image.size)) - mask = extract_mask(model_image, request.target_rgb) - return SegmentResult(mask=mask, model_image=model_image, model=model, usage=_usage(response)) + # Back to the sent size, the padding _square added dropped, and the + # target colour's pixels taken: what is left lines up with the plane + # (model_mask, which the browser's provider calls too). + mask, aligned = model_mask(np.asarray(model_image), sent.size[::-1], request.image.size[::-1], request.target_rgb) + return SegmentResult(mask=mask, model_image=Image.fromarray(aligned), model=model, usage=_usage(response)) def _generate(self, client, model, contents, config): """``generate_content``, retried on 429/503 only, at most ``MAX_ATTEMPTS`` times.""" diff --git a/cellmap_flow/ai_annotate/geometry.py b/cellmap_flow/ai_annotate/geometry.py index 58def53b..e4b958bb 100644 --- a/cellmap_flow/ai_annotate/geometry.py +++ b/cellmap_flow/ai_annotate/geometry.py @@ -1,49 +1,36 @@ """Which plane of the data an AI annotation reads, and which voxels it writes. -A click (or the view centre) picks a point in world nm and the plane the -user is looking at picks the depth axis. ``plan_plane`` turns those into a -``PlanePlan``: a square field of view around the point, snapped to the -annotation volume's voxels and clipped to the volume (the write box, one -annotation voxel thick along the depth axis), and the raw read that covers -the same physical area at the model's input voxel size (one input voxel -thick, at the point's depth). So whatever the model paints on the image -lands on the annotation voxels under it. - -Annotation voxels are placed as ``finetune.session.volume`` places them: -the volume's ``dataset_offset_nm`` is voxel 0's *centre*, and a voxel is in -a box when its centre is (``fill.box_voxels``). The raw is read on its own -grid (``io.geometry``), from the voxel that holds the first pixel's centre. +The maths is ``cellmap_flow.portable.ai_annotate``'s, which the browser runs +too, and is re-exported here under the names the server's callers use: +``plan_plane`` (a ``PlanePlan``: the square field around the point, snapped +to the annotation volume's voxels and clipped to the volume and the data, +and the raw read covering the same physical area), ``raw_box`` (that read on +the raw's grid, as an ``io.geometry.Box``) and the screen layout +(``screen_axes``, ``screen_shape``, ``to_screen``, ``from_screen``): the +image sent is laid out as the viewer's panel shows the plane. What is the +server's alone is reading which plane the viewer shows: its layout and its +display dimensions (``depth_axis_for_view``, ``display_dimensions_for_view``), +which the browser reads from its own viewer (web/src/viewer/state.ts). Axes are z, y, x throughout, as everywhere in cellmap-flow, except in the -image sent: that is laid out as the viewer's panel shows the plane -(``to_screen``), so the model, and the user reviewing its answer, see what -was on screen. Which axis runs down it and which across, and whether across -runs backwards, depend on the viewer's display dimensions (``screen_axes``): -cellmap-flow's viewers show x, y, z, so the XY plane is drawn as in z, y, x -order, the XZ plane too, and the YZ plane transposed and mirrored. +image sent (``portable/ai_annotate.py`` says how it is drawn). """ -from dataclasses import asdict, dataclass - -import numpy as np - from cellmap_flow import viewer from cellmap_flow.ai_annotate.errors import AIAnnotateError -from cellmap_flow.finetune.session.fill import box_voxels -from cellmap_flow.finetune.session.volume import volume_corner_nm from cellmap_flow.io.geometry import Box -from cellmap_flow.io.metadata import snap_integral - -AXIS_NAMES = ("z", "y", "x") - -# The longest side of an image sent to a model. A hosted image model bills -# an image by the image, not by its pixels (gemini-3-pro-image: ~560 tokens -# whatever the size), and resizes what it gets to its own working size, so -# more pixels than this cost upload time and gain nothing. -MAX_IMAGE_SIDE = 1024 - -# The plane each depth axis leaves, named by the data axes it shows. -PLANE_NAMES = ("XY", "XZ", "YZ") +from cellmap_flow.portable import ai_annotate as portable +from cellmap_flow.portable.ai_annotate import ( # noqa: F401 (re-exported for the server's callers) + AXIS_NAMES, + MAX_IMAGE_SIDE, + PLANE_NAMES, + PlanePlan, + display_names, + from_screen, + screen_axes, + screen_shape, + to_screen, +) # Neuroglancer names its cross-section panels after display-dimension # *slots*, not after the dimensions' names: the "xy" panel shows display @@ -56,70 +43,6 @@ # has a z-y slice looking along x there, and the XY plane is its "yz" panel. _LAYOUT_DEPTH_SLOT = {"xy": 2, "xz": 1, "yz": 0} -# How neuroglancer's panels draw their plane, by the display-dimension slot -# each looks along: the slot drawn down, the slot drawn across, and whether -# across runs backwards. The "xy" panel (along slot 2) draws display -# dimension 0 across and 1 down; "xz" (along 1) turns it a quarter about the -# first, so 0 across and 2 down; "yz" (along 0) a quarter about the second, -# so 1 down and 2 across, increasing to the left (AXES_RELATIVE_ORIENTATION's -# quaternions). The image sent was z, y, x order before, so planes went to -# the model, and back to the user's review, transposed or mirrored from what -# was on screen. -_PANEL_SLOTS = {2: (1, 0, False), 1: (2, 0, False), 0: (1, 2, True)} - - -def display_names(display_dimensions=None) -> tuple: - """``display_dimensions`` when they are z, y and x in some order, else - z, y, x: the display dimensions of a viewer that sets none over - cellmap-flow's coordinate space, and of a plan saved before plans kept - them.""" - names = tuple(display_dimensions or ()) - return names if sorted(names) == sorted(AXIS_NAMES) else AXIS_NAMES - - -def screen_axes(depth_axis, display_dimensions=None) -> tuple: - """``(down, across, backwards)``: the data axes (0 z, 1 y, 2 x) the panel - looking along ``depth_axis`` draws down and across, and whether across - runs backwards, in a viewer whose display dimensions are - ``display_dimensions``, as ``display_names`` gives them (a plan's), or - None for z, y, x.""" - names = tuple(display_dimensions or AXIS_NAMES) - down, across, backwards = _PANEL_SLOTS[names.index(AXIS_NAMES[int(depth_axis)])] - return AXIS_NAMES.index(names[down]), AXIS_NAMES.index(names[across]), backwards - - -def _screen(depth_axis, display_dimensions=None): - down, across, backwards = screen_axes(depth_axis, display_dimensions) - return down > across, backwards - - -def screen_shape(shape, depth_axis, display_dimensions=None) -> tuple: - """A plane's ``(rows, cols)`` in z, y, x order, as the panel draws it - (``screen_axes``).""" - transposed, _ = _screen(depth_axis, display_dimensions) - rows, cols = (int(s) for s in shape) - return (cols, rows) if transposed else (rows, cols) - - -def to_screen(plane, depth_axis, display_dimensions=None) -> np.ndarray: - """``plane`` (rows and columns its two axes in z, y, x order) as the panel - looking along ``depth_axis`` draws it, in a viewer showing - ``display_dimensions`` (``screen_axes``).""" - transposed, backwards = _screen(depth_axis, display_dimensions) - plane = np.asarray(plane) - if transposed: - plane = np.swapaxes(plane, 0, 1) - return plane[:, ::-1] if backwards else plane - - -def from_screen(image, depth_axis, display_dimensions=None) -> np.ndarray: - """``to_screen`` undone: an image as the panel draws it, in z, y, x order.""" - transposed, backwards = _screen(depth_axis, display_dimensions) - image = np.asarray(image) - if backwards: - image = image[:, ::-1] - return np.swapaxes(image, 0, 1) if transposed else image - def _layout_type(layout): """The panel name of a neuroglancer layout: a string, a dict or a state object.""" @@ -167,215 +90,22 @@ def depth_axis_for_view(viewer_state) -> int: return depth_axis_from_layout(getattr(viewer_state, "layout", None), display or None) -def _floats(values): - return tuple(float(v) for v in values) - - -def _ints(values): - return tuple(int(v) for v in values) - - -@dataclass(frozen=True) -class PlanePlan: - """One plane to annotate: what is read, what is sent and what is written. - - All three-element tuples are z, y, x. ``raw_offset_nm`` and - ``raw_shape_nm`` are the world box the raw is read over (lower corner - and size): in plane, the write box's extent; along the depth axis, one - input voxel centred on the point. ``image_shape`` is the (rows, cols) - of the image sent, laid out as on screen (``to_screen``), - ``resolution_nm`` its pixel size in nm (square - pixels, which a model needs to see shapes as they are). ``write_lo`` and - ``write_hi`` are the annotation voxels written, ``[lo, hi)``, clipped to - the volume and one voxel thick along the depth axis. - ``display_dimensions`` are the viewer's (``display_names``), which say - how its panel draws the plane (``screen_axes``); z, y, x in a plan saved - before plans kept them. - """ - - depth_axis: int - point_nm: tuple - raw_offset_nm: tuple - raw_shape_nm: tuple - image_shape: tuple - input_voxel_size: tuple - write_lo: tuple - write_hi: tuple - resolution_nm: float - display_dimensions: tuple = AXIS_NAMES - - @property - def plane_axes(self) -> tuple: - """The two in-plane axes, in z, y, x order.""" - return tuple(a for a in range(3) if a != self.depth_axis) - - @property - def plane_name(self) -> str: - """"XY", "XZ" or "YZ": the data axes the plane shows.""" - return PLANE_NAMES[self.depth_axis] - - @property - def write_shape(self) -> tuple: - """The write box's in-plane shape in annotation voxels, its ``plane_axes`` in z, y, x order.""" - return tuple(int(self.write_hi[a] - self.write_lo[a]) for a in self.plane_axes) - - @property - def click_px(self) -> tuple: - """``(row, col)`` of the point in the image sent.""" - down, across, backwards = screen_axes(self.depth_axis, self.display_dimensions) - position = [] - for side, a in zip(self.image_shape, (down, across)): - fraction = (self.point_nm[a] - self.raw_offset_nm[a]) / self.raw_shape_nm[a] - position.append(int(np.clip(np.floor(fraction * side), 0, side - 1))) - if backwards: - # The pixel counted from the near end, then mirrored, so a point - # on a boundary takes the pixel it takes in z, y, x order. - position[1] = self.image_shape[1] - 1 - position[1] - return tuple(position) - - def to_screen(self, plane) -> np.ndarray: - """``to_screen`` for this plan's plane.""" - return to_screen(plane, self.depth_axis, self.display_dimensions) - - def from_screen(self, image) -> np.ndarray: - """``from_screen`` for this plan's plane.""" - return from_screen(image, self.depth_axis, self.display_dimensions) - - def to_dict(self) -> dict: - """The plan as JSON-safe lists, for a staging's meta.json.""" - return {key: list(value) if isinstance(value, tuple) else value for key, value in asdict(self).items()} - - @classmethod - def from_dict(cls, data: dict) -> "PlanePlan": - """The plan ``to_dict`` wrote.""" - return cls( - depth_axis=int(data["depth_axis"]), - point_nm=_floats(data["point_nm"]), - raw_offset_nm=_floats(data["raw_offset_nm"]), - raw_shape_nm=_floats(data["raw_shape_nm"]), - image_shape=_ints(data["image_shape"]), - input_voxel_size=_floats(data["input_voxel_size"]), - write_lo=_ints(data["write_lo"]), - write_hi=_ints(data["write_hi"]), - resolution_nm=float(data["resolution_nm"]), - display_dimensions=display_names(data.get("display_dimensions")), - ) - - def plan_plane(volume: dict, volume_shape, point_nm, depth_axis: int, crop_size_px: int, data_box_nm=None, display_dimensions=None) -> PlanePlan: - """The plane through ``point_nm`` (world nm, z, y, x) across ``depth_axis``. - - ``volume`` is the annotation volume's record (``output_voxel_size``, - ``input_voxel_size``, ``dataset_offset_nm``), ``volume_shape`` its - ``annotation/s0`` shape. The field of view is square in nm, - ``crop_size_px`` annotation voxels along the finer in-plane axis (both, - for isotropic voxels), centred on the point; its write box is the - annotation voxels whose centres lie in it, clipped to the volume, in the - annotation voxel that holds the point along the depth axis. - - ``data_box_nm`` is the raw data's extent, ``(lo, hi)`` world nm z, y, x: - the field is clipped to it too. The volume is the data rounded up to - whole chunks of the model's output, which for Cellpose at 64 nm is up to - 33 um past the data's edge, and there the plane is only the read's - zero padding: an image of it was labelled as background, and the cells - beside it smeared into it. - - The raw is read over the clipped box's extent, so the image and the box - cover the same physical area. The image's pixels are the finer in-plane - input voxel size, square; when that makes a side longer than - ``MAX_IMAGE_SIDE`` the pixels are made larger to fit. The image is laid - out as the viewer's panel draws the plane, for the viewer's - ``display_dimensions`` (``display_dimensions_for_view``; z, y, x when - None). - - Raises AIAnnotateError ("refused") when the point is outside the volume - or the data: a field clipped to an edge it does not reach is not what - was aimed at. - """ - if depth_axis not in (0, 1, 2): - raise ValueError(f"depth_axis must be 0, 1 or 2 (z, y, x), got {depth_axis!r}") - if int(crop_size_px) < 1: - raise ValueError(f"crop_size_px must be at least 1, got {crop_size_px!r}") - output_voxel_size = np.asarray(volume["output_voxel_size"], dtype=float) - input_voxel_size = np.asarray(volume["input_voxel_size"], dtype=float) - shape = np.asarray(volume_shape, dtype=int) - corner = volume_corner_nm(volume.get("dataset_offset_nm"), output_voxel_size) - point = np.asarray(point_nm, dtype=float) - if point.shape != (3,) or not np.all(np.isfinite(point)): - raise ValueError(f"point_nm must be three finite numbers (z, y, x), got {point_nm!r}") - - voxel = np.floor(snap_integral((point - corner) / output_voxel_size)).astype(int) - if np.any(voxel < 0) or np.any(voxel >= shape): - extent = [f"{c:g}..{c + n * v:g}" for c, n, v in zip(corner, shape, output_voxel_size)] - raise AIAnnotateError( - "refused", - f"The point ({', '.join(f'{p:g}' for p in point)}) nm is outside the annotation volume " - f"(z, y, x {', '.join(extent)} nm): move the view onto the data and try again.", - http_status=400, - ) - - plane = [a for a in range(3) if a != depth_axis] - side_nm = int(crop_size_px) * float(output_voxel_size[plane].min()) - offset = point - side_nm / 2 - size = np.full(3, side_nm) - if data_box_nm is not None: - data_lo, data_hi = (np.asarray(v, dtype=float) for v in data_box_nm) - if np.any(point < data_lo) or np.any(point >= data_hi): - extent = [f"{lo:g}..{hi:g}" for lo, hi in zip(data_lo, data_hi)] - raise AIAnnotateError( - "refused", - f"The point ({', '.join(f'{p:g}' for p in point)}) nm is outside the data " - f"(z, y, x {', '.join(extent)} nm): move the view onto the data and try again.", - http_status=400, - ) - for a in plane: - top = min(offset[a] + size[a], data_hi[a]) - offset[a] = max(offset[a], data_lo[a]) - size[a] = top - offset[a] - # Along the depth axis, exactly the voxel that holds the point: a box - # one voxel long whose only centre is that voxel's. - offset[depth_axis] = corner[depth_axis] + voxel[depth_axis] * output_voxel_size[depth_axis] - size[depth_axis] = output_voxel_size[depth_axis] - box = box_voxels(volume, offset, size, shape) - if box is None: # cannot happen with the point inside, but never write an empty box - raise AIAnnotateError("refused", "No annotation voxels lie around the point.", http_status=400) - lo, hi = box - - raw_offset = corner + lo * output_voxel_size - raw_shape = (hi - lo) * output_voxel_size - raw_offset[depth_axis] = point[depth_axis] - input_voxel_size[depth_axis] / 2 - raw_shape[depth_axis] = input_voxel_size[depth_axis] - - extent = raw_shape[plane] - resolution = max(float(input_voxel_size[plane].min()), float(extent.max()) / MAX_IMAGE_SIDE) - display = display_names(display_dimensions) - image_shape = screen_shape(np.maximum(np.round(snap_integral(extent / resolution)), 1), depth_axis, display) - return PlanePlan( - depth_axis=int(depth_axis), - point_nm=_floats(point), - raw_offset_nm=_floats(snap_integral(raw_offset)), - raw_shape_nm=_floats(snap_integral(raw_shape)), - image_shape=_ints(image_shape), - input_voxel_size=_floats(input_voxel_size), - write_lo=_ints(lo), - write_hi=_ints(hi), - resolution_nm=float(snap_integral(resolution)), - display_dimensions=display, - ) + """``portable.ai_annotate.plan_plane`` (its docstring says what the plane + is), its refusal raised as the ``AIAnnotateError`` ("refused", 400) the + routes answer: a point outside the volume or the data, which the user + fixes by moving the view.""" + try: + return portable.plan_plane(volume, volume_shape, point_nm, depth_axis, crop_size_px, + data_box_nm=data_box_nm, display_dimensions=display_dimensions) + except portable.Refused as e: + raise AIAnnotateError("refused", str(e), http_status=400) from None def raw_box(plan: PlanePlan, voxel_size, corner) -> Box: - """The raw voxels to read for ``plan``, on a raw grid of ``voxel_size`` from ``corner``. - - Each axis starts at the voxel that holds the first sample's centre (half - a voxel in from the box's corner) and runs for as many voxels as fit in - the box, at least one: so a box on the grid reads exactly its voxels, - and the depth axis reads the voxel that holds the point. - """ - voxel_size = np.asarray(voxel_size, dtype=float) - corner = np.asarray(corner, dtype=float) - first_centre = np.asarray(plan.raw_offset_nm, dtype=float) + voxel_size / 2 - begin = np.floor(snap_integral((first_centre - corner) / voxel_size)).astype(int) - count = np.maximum(np.round(snap_integral(np.asarray(plan.raw_shape_nm) / voxel_size)), 1).astype(int) - return Box(_ints(begin), _ints(count)) + """The raw voxels to read for ``plan``, on a raw grid of ``voxel_size`` + from ``corner`` (its voxel 0's lower corner), as an ``io.geometry.Box`` + (``portable.ai_annotate.raw_box``).""" + begin, count = portable.raw_box(plan, voxel_size, corner) + return Box(begin, count) diff --git a/cellmap_flow/ai_annotate/mask_extraction.py b/cellmap_flow/ai_annotate/mask_extraction.py index bbb7b3b8..44ca5110 100644 --- a/cellmap_flow/ai_annotate/mask_extraction.py +++ b/cellmap_flow/ai_annotate/mask_extraction.py @@ -3,7 +3,11 @@ Adapted from ask-to-mask (``_normalize_to_uint8``/``_slice_to_rgb`` and ``extract_mask``'s colour-difference mode): the model is asked to paint the structure one colour and everything else black, and the mask is the pixels -that came back that colour. +that came back that colour. The arithmetic is +``cellmap_flow.portable.ai_annotate``'s (``normalize_to_uint8``, +``extract_mask``; ``model_mask`` lines a reply up with the plane first), +which the browser runs too; here are the Pillow images the server's +backends and staging pass around. """ from __future__ import annotations @@ -11,21 +15,8 @@ import numpy as np from PIL import Image - -def normalize_to_uint8(data: np.ndarray) -> np.ndarray: - """A 2D array as uint8 grey levels. - - uint8 passes through unchanged. Anything else is clipped to its 1st-99th - percentile over the plane and stretched to 0-255, as raw EM is not always - 8-bit and a few saturated pixels should not flatten the contrast. - """ - if data.dtype == np.uint8: - return data - p_lo, p_hi = np.percentile(data, (1, 99)) - if p_hi <= p_lo: - p_hi = p_lo + 1 - clipped = np.clip(data.astype(np.float32), p_lo, p_hi) - return ((clipped - p_lo) / (p_hi - p_lo) * 255).astype(np.uint8) +from cellmap_flow.portable import ai_annotate as portable +from cellmap_flow.portable.ai_annotate import normalize_to_uint8 # noqa: F401 (re-exported) def slice_to_rgb(data_2d: np.ndarray) -> Image.Image: @@ -35,37 +26,12 @@ def slice_to_rgb(data_2d: np.ndarray) -> Image.Image: def extract_mask( - output_image: Image.Image, + output_image, target_rgb: tuple[int, int, int], threshold: float = 200.0, ) -> np.ndarray: - """The pixels of the model's reply painted ``target_rgb``, as a bool (H, W) mask. - - Each pixel scores how strongly it shows the target colour: its weakest - channel among those the target has at full strength, minus its strongest - deviation elsewhere -- a channel the target has off (0) being on, or a - channel the target has at part strength being away from that strength. - For red (255, 0, 0) that is ``R - max(G, B)``: red that dominates. A - pixel counts when its score reaches ``threshold``. - - The part-strength term is what lets orange (255, 128, 0) or purple - (128, 0, 255) be found at all: counting their half channel as "on" would - cap the score at 128, under the threshold, and counting it as "off" - would reject the colour itself. - """ + """The pixels of the model's reply (a Pillow image or an RGB array) + painted ``target_rgb``, as a bool (H, W) mask + (``portable.ai_annotate.extract_mask`` says how a pixel is scored).""" out = np.asarray(output_image.convert("RGB") if isinstance(output_image, Image.Image) else output_image) - out = out.astype(np.float32) - target = np.asarray(target_rgb, dtype=np.float32) - - full = target >= 255 - off = target <= 0 - part = ~full & ~off - - strength = np.min(out[:, :, full], axis=-1) if full.any() else np.full(out.shape[:2], 255.0, np.float32) - deviations = [] - if off.any(): - deviations.append(np.max(out[:, :, off], axis=-1)) - if part.any(): - deviations.append(np.max(np.abs(out[:, :, part] - target[part]), axis=-1)) - score = strength - np.max(deviations, axis=0) if deviations else strength - return score >= threshold + return portable.extract_mask(out, target_rgb, threshold) diff --git a/cellmap_flow/ai_annotate/pipeline.py b/cellmap_flow/ai_annotate/pipeline.py index 4ec7eaa1..7623a459 100644 --- a/cellmap_flow/ai_annotate/pipeline.py +++ b/cellmap_flow/ai_annotate/pipeline.py @@ -4,31 +4,24 @@ grid it planned and laid out as on screen; ``build_request`` makes it the request a backend sends; ``mask_to_write_shape`` cuts the mask the backend returns into its connected objects and puts them onto the write box's -voxels, and ``labels_for_box`` turns those into the labels ``fill.paint_box`` -writes: one id per object, background elsewhere, as a seed from a model's -prediction is labelled. +voxels, and ``labels_for_box`` (``accept_labels`` for Accept) turns those +into the labels ``fill.paint_box`` writes: one id per object, background +elsewhere, as a seed from a model's prediction is labelled. + +The maths is ``cellmap_flow.portable.ai_annotate``'s, which the browser runs +too (its resampling is Pillow's, in both modes); what is the server's alone +is reading the raw through ``ImageDataInterface``. """ import numpy as np -from PIL import Image - -from cellmap_flow.ai_annotate.geometry import PlanePlan, raw_box, screen_shape - -def _resize(plane: np.ndarray, shape) -> np.ndarray: - """``plane`` resampled to ``shape`` (rows, cols); itself when it already is that. - - Shrinking averages each pixel's footprint (PIL's box filter), so fine - membranes fade rather than alias; growing (the coarse axis of an - anisotropic plane) interpolates linearly. - """ - rows, cols = (int(s) for s in shape) - if plane.shape == (rows, cols): - return plane - shrinking = plane.shape[0] >= rows and plane.shape[1] >= cols - image = Image.fromarray(np.ascontiguousarray(plane, dtype=np.float32)) - resized = image.resize((cols, rows), Image.BOX if shrinking else Image.BILINEAR) - return np.asarray(resized, dtype=np.float32) +from cellmap_flow.ai_annotate.geometry import PlanePlan, raw_box +from cellmap_flow.portable.ai_annotate import ( # noqa: F401 (re-exported for the routes) + accept_labels, + labels_for_box, + mask_to_write_shape, + plane_image, +) def _raw(volume: dict, voxel_size): @@ -53,7 +46,7 @@ def raw_extent_nm(volume: dict): def read_plane(volume: dict, plan: PlanePlan) -> np.ndarray: """The raw plane ``plan`` reads, as a 2D array of ``plan.image_shape``, - laid out as on screen (``PlanePlan.to_screen``). + laid out as on screen (``portable.ai_annotate.plane_image``). Read as stored (no input normalization: the image is scaled to 8 bits for the model on its own) at the plan's input voxel size, from the level @@ -64,9 +57,7 @@ def read_plane(volume: dict, plan: PlanePlan) -> np.ndarray: """ idi = _raw(volume, plan.input_voxel_size) box = raw_box(plan, idi.voxel_size, idi.offset) - data = np.asarray(idi.read_box(box)) - plane = np.take(data, 0, axis=plan.depth_axis) - return _resize(plan.to_screen(plane), plan.image_shape) + return plane_image(np.asarray(idi.read_box(box)), plan) def build_request(plane: np.ndarray, plan: PlanePlan, profile, prompt_override=None): @@ -89,81 +80,3 @@ def build_request(plane: np.ndarray, plan: PlanePlan, profile, prompt_override=N target_rgb=tuple(int(c) for c in profile.rgb), click_px=plan.click_px, ) - - -def mask_to_write_shape(mask: np.ndarray, plan: PlanePlan) -> np.ndarray: - """The objects of ``mask`` (the image's shape, as on screen) on the write - box's in-plane voxels, in z, y, x order (``PlanePlan.write_shape``): - int32, 0 for background and 1..n for the objects, numbered in the box's - raster order as ``scipy.ndimage.label`` numbers a mask. - - Objects are the mask's 4-connected pieces (scipy's default, so pieces - touching only at a corner stay apart, as the model was asked to leave - them), found at the image's own resolution, where the model drew the - lines between them: on voxels coarser than the pixels a line can fall - between the pixels the voxels take, and the objects either side of it - still keep ids of their own. Then nearest neighbour, pixel centre to - voxel centre: each annotation voxel takes the object of the pixel at - its centre, so the mask keeps hard edges and lands where it was drawn - whatever the two sizes are. An object no voxel centre falls in is lost. - """ - from scipy.ndimage import label - - mask = np.asarray(mask, dtype=bool) - if mask.ndim != 2: - raise ValueError(f"The mask must be 2D, got shape {mask.shape}") - objects, _ = label(mask) - rows, cols = screen_shape(plan.write_shape, plan.depth_axis, plan.display_dimensions) - if objects.shape != (rows, cols): - objects = np.asarray(Image.fromarray(objects.astype(np.int32)).resize((cols, rows), Image.NEAREST)) - objects = plan.from_screen(objects) - # Renumbered by first voxel, so fresh ids go to the objects in the order - # labelling the box itself would give them, whatever the screen layout. - ids, first = np.unique(objects, return_index=True) - order = ids[ids > 0][np.argsort(first[ids > 0])] - number = np.zeros(int(objects.max(initial=0)) + 1, dtype=np.int32) - number[order] = np.arange(1, order.size + 1) - return number[objects] - - -def labels_for_box(objects: np.ndarray, existing: np.ndarray, depth_axis: int, count_up: bool, - beside=None) -> np.ndarray: - """The labels for the write box: an id per object of ``objects``, 1 elsewhere. - - ``objects`` is the in-plane objects (``mask_to_write_shape``), 0 for - background, ``existing`` the one-voxel-thick box as the volume holds - it. The objects are labelled by ``fill.seed_labels``: an object the user - has already painted part of keeps that id, the rest get fresh ones, - counting up for an instance target's ``count_up``. - - ``beside`` is the planes on either side of the box along the depth axis, - as the volume holds them (each shaped like ``existing``, or None at the - volume's edge). Planes are annotated one at a time, so an object usually - continues one in the plane next to it: where this plane is unpainted, - the ids beside it count as painted here, and an object takes the id it - overlaps most, rather than a fresh one that an instance target would - read as a boundary between the two planes. Fresh ids skip those planes' - ids too, so a new object does not take the id of a different object - right next to it, which an instance target would read as one object. - """ - from cellmap_flow.finetune.session.fill import seed_labels - - objects = np.expand_dims(np.asarray(objects), depth_axis) - if objects.shape != existing.shape: - raise ValueError(f"The mask makes a box of {objects.shape}; the box holds {existing.shape}") - planes = [np.asarray(p, dtype=existing.dtype) for p in (beside or ()) if p is not None] - if any(p.shape != existing.shape for p in planes): - raise ValueError(f"The planes beside the box must be shaped {existing.shape}") - if not planes: - return seed_labels(objects, existing, count_up=count_up) - seen = existing.copy() - for plane in planes: - carry = (seen == 0) & (plane >= 2) - seen[carry] = plane[carry] - # The planes beside are stacked after the box with no objects in them: - # seed_labels labels only the box's objects, but its fresh ids see every - # id in the stack. - stack_existing = np.concatenate([seen, *planes], axis=depth_axis) - stack_objects = np.concatenate([objects, *[np.zeros_like(objects)] * len(planes)], axis=depth_axis) - labels = seed_labels(stack_objects, stack_existing, count_up=count_up) - return np.take(labels, [0], axis=depth_axis) diff --git a/cellmap_flow/ai_annotate/staging.py b/cellmap_flow/ai_annotate/staging.py index 18e9f8d7..28215765 100644 --- a/cellmap_flow/ai_annotate/staging.py +++ b/cellmap_flow/ai_annotate/staging.py @@ -26,6 +26,7 @@ from PIL import Image from cellmap_flow.ai_annotate.errors import AIAnnotateError +from cellmap_flow.portable import ai_annotate as portable ANNOTATE_ID_RE = re.compile(r"^[0-9a-f]{32}$") STAGING_DIRNAME = ".ai_annotate" @@ -36,7 +37,7 @@ META_JSON = "meta.json" # How strongly the overlay tints the mask with the target colour. -OVERLAY_ALPHA = 0.5 +OVERLAY_ALPHA = portable.OVERLAY_ALPHA def new_annotate_id() -> str: @@ -142,14 +143,10 @@ def overlay(input_image: Image.Image, mask_write: np.ndarray, target_rgb) -> Ima The mask is the write box's, laid out as the image is (``to_screen``) and scaled to it by nearest neighbour, so the overlay shows what - accepting writes, not the model's own image. + accepting writes, not the model's own image + (``portable.ai_annotate.overlay``, which the browser's review uses too). """ - rgb = np.asarray(input_image.convert("RGB"), dtype=np.float32) - mask = Image.fromarray(np.asarray(mask_write, dtype=np.uint8) * 255).resize(input_image.size, Image.NEAREST) - on = np.asarray(mask) > 127 - tint = np.asarray(target_rgb, dtype=np.float32) - rgb[on] = (1 - OVERLAY_ALPHA) * rgb[on] + OVERLAY_ALPHA * tint - return Image.fromarray(np.round(rgb).astype(np.uint8)) + return Image.fromarray(portable.overlay(np.asarray(input_image.convert("RGB")), mask_write, target_rgb, OVERLAY_ALPHA)) def preview(corrections_dir, annotate_id) -> dict: diff --git a/cellmap_flow/dashboard/routes/finetune/ai_annotate.py b/cellmap_flow/dashboard/routes/finetune/ai_annotate.py index 43480e45..0a08168e 100644 --- a/cellmap_flow/dashboard/routes/finetune/ai_annotate.py +++ b/cellmap_flow/dashboard/routes/finetune/ai_annotate.py @@ -687,7 +687,7 @@ def _resend_job(ai, job, config, segment_request): def _planes_beside(state, volume_id, lo, hi, depth_axis): """The planes on either side of the box ``[lo, hi)`` along ``depth_axis``, as MinIO serves them. - For ``pipeline.labels_for_box``; None for a side past the volume's edge. + For ``pipeline.accept_labels``; None for a side past the volume's edge. Read just before the write, so a stroke painted in them since the plane was staged counts. """ @@ -742,20 +742,14 @@ def ai_annotate_accept(): beside = _planes_beside(state, volume_id, lo, hi, plan.depth_axis) def labels_for(existing): - # As a seed: an instance target on a uint16/uint32 volume counts + # As a seed (an instance target on a uint16/uint32 volume counts # its ids up, a uint8 volume reuses free ones; an object - # continuing one in the plane beside keeps its id. - labels = pipeline.labels_for_box( - mask_write, existing, plan.depth_axis, - count_up=instance_target and existing.dtype.itemsize > 1, beside=beside, - ) - if body.overwrite: - # Overwrite lets the model's objects replace what is under - # them, but its background only fills unlabelled voxels: an - # object already painted that the model missed is kept, not - # erased to background. - labels[(labels == 1) & (existing != 0)] = 0 - return labels + # continuing one in the plane beside keeps its id), and with + # overwrite the model's background only filling unlabelled + # voxels: portable/ai_annotate.py's accept_labels, which the + # browser's Accept runs too. + return pipeline.accept_labels(mask_write, existing, plan.depth_axis, instance_target, body.overwrite, + beside=beside) n_foreground, n_background, n_overwritten = paint_box( state, volume_id, lo, hi, labels_for, volume.get("zarr_path"), @@ -769,7 +763,7 @@ def labels_for(existing): logger.warning(f"Could not accept AI annotation {job['annotate_id']}: {e}") return _error("The staged annotation or the served volume is missing; reject it and run again.", 409) if isinstance(e, ValueError): - # Our own messages (labels_for_box, fill's ids running out), which + # Our own messages (accept_labels, fill's ids running out), which # say what to do: the seed route answers them the same way. logger.warning(f"Could not accept AI annotation {job['annotate_id']}: {e}") return _error(str(e), 409) diff --git a/cellmap_flow/dashboard/routes/finetune/common.py b/cellmap_flow/dashboard/routes/finetune/common.py index 2d941ffa..f8a0904c 100644 --- a/cellmap_flow/dashboard/routes/finetune/common.py +++ b/cellmap_flow/dashboard/routes/finetune/common.py @@ -12,7 +12,11 @@ from cellmap_flow.finetune.session.minio import MINIO_PROXY_URL_ENV, proxied_url from cellmap_flow.finetune.session.store import SessionStore from cellmap_flow.models.geometry import channel_names_of +# INSTANCE_TARGETS: the targets trained on instances, whose annotation +# volumes hold an id per object; imported from here by the routes. +from cellmap_flow.portable.volume import INSTANCE_TARGETS, annotation_dtype # noqa: F401 from cellmap_flow.serving.client import fetch_model_info, running_job_host +from cellmap_flow.portable.label_it import affinity_offsets logger = logging.getLogger(__name__) @@ -191,25 +195,18 @@ def declared_output_type(model_config): return None -# The targets trained on instances, whose annotation volumes hold an id per -# object rather than foreground and background alone. -INSTANCE_TARGETS = ("affinities", "flows") - - def annotation_dtype_for(model_config): - """The labels' dtype of a new annotation volume for ``model_config``. - - uint8 (ids up to 255) for a model trained on foreground and background; - uint16 (up to 65535) for one trained on instances (affinities, Cellpose's - flows), whose seeds number each object: 282 of a Cellpose model's masks - in one box did not fit a uint8 volume. - """ + """The labels' dtype of a new annotation volume for ``model_config``: + ``cellmap_flow.portable.volume.annotation_dtype`` (the browser's rule too) + of what it trains on (``autodetect_output_type``), uint16 for the + instance targets in INSTANCE_TARGETS, else uint8; uint8 when that cannot + be told.""" try: output_type, _ = autodetect_output_type(model_config, None, None) except Exception as e: logger.info(f"Could not tell what {getattr(model_config, 'name', 'the model')} trains on ({e}): uint8 labels") return "uint8" - return "uint16" if output_type in INSTANCE_TARGETS else "uint8" + return annotation_dtype(output_type) def autodetect_output_type(model_config, output_type, offsets): @@ -265,13 +262,13 @@ def autodetect_output_type(model_config, output_type, offsets): except Exception as e: logger.debug(f"Could not resolve channels for autodetect: {e}") - if channels and any("_aff" in channel for channel in channels): + # The one rule for a model known by its channel names alone + # (portable/label_it.affinity_offsets): the browser's Python + # applies it to its manifest's names, for training and seeding, + # so both modes treat such a model the same way. + default_offsets = affinity_offsets(channels) if channels else None + if default_offsets is not None: resolved_output_type = "affinities" - n_aff = sum(1 for channel in channels if "_aff" in channel) - default_offsets = [ - [1 if axis == index else 0 for axis in range(3)] - for index in range(min(n_aff, 3)) - ] resolved_offsets = json.dumps(default_offsets) logger.info( f"Auto-detected output_type='affinities' from " diff --git a/cellmap_flow/dashboard/routes/finetune/good_regions.py b/cellmap_flow/dashboard/routes/finetune/good_regions.py index 1fe5755e..2ee2c3f5 100644 --- a/cellmap_flow/dashboard/routes/finetune/good_regions.py +++ b/cellmap_flow/dashboard/routes/finetune/good_regions.py @@ -18,7 +18,6 @@ """ import logging -import uuid import neuroglancer import numpy as np @@ -28,20 +27,13 @@ from cellmap_flow.dashboard.routes.finetune.common import session_store, viewer_position_and_scales from cellmap_flow.dashboard.state import get_session from cellmap_flow.finetune.session import manifest as session_manifest +from cellmap_flow.portable import regions as portable_regions from cellmap_flow.viewer.layers import replace_layer logger = logging.getLogger(__name__) GOOD_REGIONS_LAYER = "good_regions" -# Used when neither the request nor the active volume says how big a region -# should be. One model *output* patch is the natural unit: it is what you can -# actually see and judge on screen, and it is the region a training patch -# computes loss over. Sizing to the input field of view instead would claim -# the model is right across 2848nm when you only looked at the middle 896nm -# of it -- and would not match annotation crops, which are already write_shape. -DEFAULT_REGION_SIZE_NM = 896.0 - def _active_volume(): """The annotation volume currently being worked on, or None.""" @@ -87,28 +79,16 @@ def save_good_regions(regions): return session_manifest.save_good_regions(path, regions) -def _default_size_nm(): - """One model output patch, in nm -- see DEFAULT_REGION_SIZE_NM.""" - volume = _active_volume() or {} - output_size = volume.get("output_size") - output_voxel_size = volume.get("output_voxel_size") - if output_size and output_voxel_size: - try: - return (np.array(output_size, dtype=float) - * np.array(output_voxel_size, dtype=float)).tolist() - except (TypeError, ValueError): - pass - return [DEFAULT_REGION_SIZE_NM] * 3 - - def view_box_nm(size_nm=None): """``(centre_nm, size_nm)`` of the box centred where the viewer looks. - ``size_nm`` defaults to one model output patch (see - DEFAULT_REGION_SIZE_NM); one number is the same along every axis. - The labelling buttons beside "Mark This View as Good" use the same box, - so what they label is what a mark would cover. ValueError when there - is no viewer position or the size is not positive. + ``size_nm`` is as ``cellmap_flow.portable.regions.region_size_nm`` (the + browser's rule too) takes it: one model output patch of the active + volume when none is given (see DEFAULT_REGION_SIZE_NM there), one number + the same along every axis. The labelling buttons beside "Mark This View + as Good" use the same box, so what they label is what a mark would + cover. ValueError when there is no viewer position or the size is not + positive. """ position, scales_nm = viewer_position_and_scales() if position is None: @@ -120,12 +100,9 @@ def view_box_nm(size_nm=None): if scales_nm: centre_nm = centre_nm * np.array(scales_nm, dtype=float) - size_nm = np.array(size_nm or _default_size_nm(), dtype=float) - if size_nm.size == 1: - size_nm = np.repeat(size_nm, 3) - if np.any(size_nm <= 0): - raise ValueError("size_nm must be positive") - return centre_nm, size_nm + volume = _active_volume() or {} + size_nm = portable_regions.region_size_nm(size_nm, volume.get("output_size"), volume.get("output_voxel_size")) + return centre_nm, np.array(size_nm, dtype=float) @finetune_bp.route("/api/finetune/good-regions/mark-view", methods=["POST"]) @@ -135,15 +112,10 @@ def mark_current_view_good(): try: centre_nm, size_nm = view_box_nm(data.get("size_nm")) - regions = load_good_regions() - region = { - "id": uuid.uuid4().hex[:8], - "label": data.get("label") or f"good-{len(regions) + 1}", - "offset_nm": (centre_nm - size_nm / 2).tolist(), - "shape_nm": size_nm.tolist(), - } - regions.append(region) - if not save_good_regions(regions): + stored = load_good_regions() + region = portable_regions.new_region(centre_nm, size_nm, len(stored), data.get("label")) + stored.append(region) + if not save_good_regions(stored): # Drawing the box anyway is what made this look like it worked. # A mark that is not on disk will not reach training, and the # next click will silently overwrite it, so say so instead. @@ -157,16 +129,16 @@ def mark_current_view_good(): "needs_volume": True, }), 409 - refresh_good_regions_layer(regions) + refresh_good_regions_layer(stored) logger.info( f"Marked good region {region['label']} at " f"{[round(v) for v in centre_nm]} nm, size {[round(v) for v in size_nm]} nm " - f"({len(regions)} total)" + f"({len(stored)} total)" ) return jsonify({ "success": True, "region": region, - "count": len(regions), + "count": len(stored), "persisted": True, }) except ValueError as e: @@ -202,7 +174,7 @@ def refresh_good_regions_layer(regions=None): session = get_session() if session.viewer is None: return 0 - regions = load_good_regions() if regions is None else regions + entries = load_good_regions() if regions is None else regions axes_names = ["z", "y", "x"] try: @@ -214,18 +186,20 @@ def refresh_good_regions_layer(regions=None): pass annotations = [] - for index, region in enumerate(regions): - try: - lo = np.array(region["offset_nm"], dtype=float) - hi = lo + np.array(region["shape_nm"], dtype=float) - except (KeyError, TypeError, ValueError): + for index, region in enumerate(entries): + # An entry that is not a region is skipped and keeps its index, as + # the browser's layer skips it (portable_regions.region_box_nm). + box = portable_regions.region_box_nm(region) + if box is None: continue + lo, shape = box + label = region.get("label") annotations.append( neuroglancer.AxisAlignedBoundingBoxAnnotation( point_a=lo.tolist(), - point_b=hi.tolist(), + point_b=(lo + shape).tolist(), id=str(index), - description=region.get("label", "good"), + description=label if isinstance(label, str) else "good", ) ) diff --git a/cellmap_flow/dashboard/routes/finetune/view_labels.py b/cellmap_flow/dashboard/routes/finetune/view_labels.py index ff792f64..f3468c18 100644 --- a/cellmap_flow/dashboard/routes/finetune/view_labels.py +++ b/cellmap_flow/dashboard/routes/finetune/view_labels.py @@ -14,6 +14,11 @@ - POST ``/api/finetune/view-labels/background``: every unannotated voxel 1, for a region the model fills with false positives. +A seed's maths (its options, the channels it reads, where the prediction +lies on the paint's grid, the probability and the components method) is +``cellmap_flow/portable/label_it.py``'s, which the browser runs too; this +module reads the model's server and writes the paint around it. + Both fill only unannotated voxels (``session.fill``): a voxel the user painted is a decision, a seed is a guess. The write goes to MinIO, which neuroglancer reads, and the changed chunks are pulled to disk at once. @@ -36,7 +41,6 @@ import requests import zarr from flask import jsonify, request -from scipy.special import expit from cellmap_flow.dashboard.finetune_utils import sync_annotation_volume_from_minio from cellmap_flow.dashboard.routes.finetune.blueprint import finetune_bp @@ -50,12 +54,11 @@ from cellmap_flow.dashboard.routes.finetune.overlay import refresh_annotation_layer from cellmap_flow.dashboard.state import get_session from cellmap_flow.finetune.session import fill -from cellmap_flow.finetune.session.volume import volume_corner_nm -from cellmap_flow.io.ome import ome_corner from cellmap_flow.pipeline_spec import PipelineSpec +from cellmap_flow.portable import label_it +from cellmap_flow.portable.label_it import SEED_METHODS, probability from cellmap_flow.post import segment from cellmap_flow.serving.client import fetch_model_info -from cellmap_flow.serving.probe import SIGNED_UNIT, UNIT, classify_output_range from cellmap_flow.serving.protocol import ARGS_KEY from cellmap_flow.viewer.layers import prediction_url, prediction_voxel_override @@ -72,24 +75,6 @@ # not answer by then is left out of the methods until it does. METADATA_TIMEOUT_SECONDS = 5 -# How a seed makes objects of the prediction (``post.segment``), each fitting -# a kind of output; ``seed_methods`` says which a model offers. -SEED_METHODS = ( - # Threshold, then an id per connected object: any output with a - # foreground threshold. The default. - "components", - # Affinity models: their offset channels, joined above the threshold. - "mutex_watershed", - # Distance models: touching objects cut apart where the distance dips. - "distance_watershed", - # A server that serves integer instance labels (Cellpose's masks). - "instances", -) - -# Training targets made of instances, where ids must differ between nearby -# objects (finetune.cli's output types). - - class _Refused(Exception): """A request these routes will not act on: ``status``, a message and extra answer fields.""" @@ -396,9 +381,9 @@ def read_prediction(host, model_name, volume, lo, hi, info, postprocess=(), inpu raw output, and that is where its decision boundary is. (``postprocess`` asks for one anyway: Cellpose's masks of a flows server's output, for a seed of its instances.) Each annotation - voxel takes the served voxel its centre lies in, placed where - neuroglancer draws the layer (``prediction_voxel_override``), so the - seed lies under the prediction the user sees. + voxel takes the served voxel its centre lies in (``label_it.placement``), + placed where neuroglancer draws the layer (``prediction_voxel_override``), + so the seed lies under the prediction the user sees. Returns ``(prediction, lo, hi)``: a (channels, z, y, x) array, float32 or, when the server serves integer labels, as served (an id past 2^24 @@ -416,34 +401,25 @@ def read_prediction(host, model_name, volume, lo, hi, info, postprocess=(), inpu scale = transforms["scale"]["scale"] # A translation of 0 may be left out. translation = transforms.get("translation", {}).get("translation", [0.0] * len(names)) - declared = np.asarray([scale[i] for i in spatial], dtype=float) - corner_nm = np.asarray(ome_corner([translation[i] for i in spatial], declared)) - drawn = np.asarray( - prediction_voxel_override(host, get_session().dataset_path, info) or declared, dtype=float - ) - # Neuroglancer keeps the served offset in voxels when it redraws the - # layer at another voxel size. - served_corner_nm = corner_nm / declared * drawn + declared = [scale[i] for i in spatial] + # The voxel size neuroglancer draws the layer at, when not the declared one. + drawn = prediction_voxel_override(host, get_session().dataset_path, info) or None meta = _get(f"{base}/s0/.zarray").json() shape = [meta["shape"][i] for i in spatial] - - voxel_size = np.asarray(volume["output_voxel_size"], dtype=float) - volume_corner = volume_corner_nm(volume.get("dataset_offset_nm"), voxel_size) - indices, new_lo, new_hi = [], lo.copy(), hi.copy() - for axis in range(3): - centres = volume_corner[axis] + (np.arange(lo[axis], hi[axis]) + 0.5) * voxel_size[axis] - index = np.floor((centres - served_corner_nm[axis]) / drawn[axis]).astype(int) - inside = np.flatnonzero((index >= 0) & (index < shape[axis])) - if not inside.size: - raise _Refused("The view is outside what the model's server predicts.", 400) - new_lo[axis], new_hi[axis] = lo[axis] + inside[0], lo[axis] + inside[-1] + 1 - indices.append(index[inside]) + placed = label_it.placement( + lo, hi, volume["output_voxel_size"], volume.get("dataset_offset_nm"), + [translation[i] for i in spatial], declared, shape, drawn, + ) + if placed is None: + raise _Refused("The view is outside what the model's server predicts.", 400) + indices, new_lo, new_hi = placed + read_lo, read_hi = label_it.read_box(indices) # Fetched together, then read through zarr as the array they belong to. # Every channel is in one chunk, index 0. chunk = np.asarray([meta["chunks"][i] for i in spatial]) - first = np.array([i[0] for i in indices]) // chunk - last = np.array([i[-1] for i in indices]) // chunk + first = np.asarray(read_lo) // chunk + last = (np.asarray(read_hi) - 1) // chunk separator = meta.get("dimension_separator", ".") keys = [] for offset in np.ndindex(*(last - first + 1)): @@ -456,93 +432,55 @@ def read_prediction(host, model_name, volume, lo, hi, info, postprocess=(), inpu store = {".zarray": json.dumps(meta).encode(), **dict(zip(keys, contents))} served = zarr.open_array(store=store, mode="r") window = [slice(None)] * len(names) - for axis, index in zip(spatial, indices): - window[axis] = slice(index[0], index[-1] + 1) + for axis, a, b in zip(spatial, read_lo, read_hi): + window[axis] = slice(a, b) region = served[tuple(window)] region = region[None] if channel is None else np.moveaxis(region, channel, 0) - prediction = region[(slice(None),) + np.ix_(*(i - i[0] for i in indices))] + prediction = label_it.gather(region, indices) if prediction.dtype.kind not in "iu": prediction = prediction.astype(np.float32) - return prediction, new_lo, new_hi - - -def probability(prediction, output_class=None, channels=None): - """The prediction as a probability, (channels, z, y, x), 0.5 at the model's decision boundary. - - The boundary depends on the output's activation: 0.5 on [0, 1] output - (a sigmoid already applied; the cellmap distance models end in one, and - 0.5 is their object boundary), 0 on tanh's [-1, 1], and 0 on unbounded - output, logits or a signed distance alike. Each is turned into a - probability, so ``channels`` -- the affinity channels, say -- can be - averaged and one threshold means the same for every model. - ``output_class`` is the server's (serving.probe), or read off the - values when it has none. - """ - if channels is not None: - prediction = prediction[channels] - if not prediction.shape[0]: - raise ValueError(f"The model has no channel {channels.start}") - prediction = prediction.astype(np.float32, copy=False) - output_class = output_class or classify_output_range(float(prediction.min()), float(prediction.max())) - if output_class == UNIT: - return prediction - if output_class == SIGNED_UNIT: - return (prediction + 1) / 2 - return expit(prediction) - - -def foreground_mask(prediction, output_class=None, channels=None, threshold=0.5): - """Where the model says foreground: its ``probability``, averaged over ``channels``, over ``threshold``. - - ``threshold`` is in probability whatever the activation: 0.5 is the - model's own boundary. - """ - return probability(prediction, output_class, channels).mean(axis=0) > threshold + return prediction, np.asarray(new_lo), np.asarray(new_hi) def _seed_plan(base_model, select_channel): """``(channels, output_type, offsets)``: the channels a seed reads, and what the model outputs. - As the trainer targets them: the selected channel; else an affinity - model's offset channels (the rest, LSDs say, are masked out of its - loss); else every channel, which a binary target is broadcast over. - ``output_type`` and ``offsets`` are ``model_output``'s. + ``label_it.seed_channels``' (the selected channel; else an affinity + model's offset channels; else every one), with the server's one case + more: a Cellpose flows model's probability channel. ``output_type`` and + ``offsets`` are ``model_output``'s. """ output_type, offsets = model_output(base_model) - if select_channel is not None: - channel = int(select_channel) - if channel < 0: - raise ValueError(f"select_channel must be 0 or more, got {channel}") - return slice(channel, channel + 1), output_type, offsets - if output_type == "affinities": - return slice(0, len(json.loads(offsets))), output_type, offsets - if _serves_cellpose_flows(base_model): + channels = label_it.seed_channels(output_type, json.loads(offsets) if offsets else None, select_channel) + if channels is None and _serves_cellpose_flows(base_model): # Thresholding reads the probability, not the flows beside it: after # flow_y and flow_x (and flow_z first in 3D), as the config says. channel = getattr(find_model_config(base_model), "display_channel", None) channel = 2 if channel is None else channel return slice(channel, channel + 1), output_type, offsets - return None, output_type, offsets + return channels, output_type, offsets def segment_prediction(method, prediction, output_class, channels, offsets, threshold=0.5, min_size=0, connectivity=1, per_slice=False): """The objects ``method`` makes of the prediction: 0 background, 1..n (``post.segment``). - ``threshold`` is a probability (``probability``) for every method that - thresholds: the foreground boundary for components and the distance - watershed, and the mutex watershed's bias, the affinity above which - two voxels join. ``connectivity`` and ``per_slice`` are the connected - components'; the watersheds take ``connectivity`` too. ``instances`` - splits an id's disconnected parts: a server numbers each chunk's - objects on its own (Cellpose's from 1), and the box can span chunks. + ``threshold`` is a probability (``label_it.probability``) for every + method that thresholds: the foreground boundary for components and the + distance watershed, and the mutex watershed's bias, the affinity above + which two voxels join. ``connectivity`` and ``per_slice`` are the + connected components'; the watersheds take ``connectivity`` too. + ``instances`` splits an id's disconnected parts: a server numbers each + chunk's objects on its own (Cellpose's from 1), and the box can span + chunks. Components and the mutex watershed's input are + ``label_it``'s, as the browser's are; the mutex watershed itself, the + distance watershed and the instances are the server's. """ if method == "components": - mask = foreground_mask(prediction, output_class, channels, threshold) - return segment.connected_components(mask, connectivity, min_size, per_slice) + return label_it.components(prediction, output_class, channels, threshold, min_size, connectivity, per_slice) if method == "mutex_watershed": offsets = json.loads(offsets) - affinities = probability(prediction, output_class, slice(0, len(offsets))) + affinities = label_it.affinities(prediction, output_class, offsets) return segment.mutex_watershed(affinities, offsets, bias=threshold, min_size=min_size) if method == "distance_watershed": distance = probability(prediction, output_class, channels).mean(axis=0) @@ -573,17 +511,9 @@ def seed_view_from_prediction(): """ data = request.get_json(silent=True) or {} try: - threshold = data.get("threshold") - if threshold is not None and not 0 <= float(threshold) <= 1: - raise _Refused(f"threshold must be between 0 and 1, got {threshold}", 400) - min_size = int(data.get("min_size") or 0) - if min_size < 0: - raise _Refused(f"min_size cannot be negative, got {min_size}", 400) - method = data.get("method") or "components" - if method not in SEED_METHODS: - raise _Refused(f"method must be one of {', '.join(SEED_METHODS)}, got {method!r}", 400) - connectivity = segment.as_connectivity(data.get("connectivity", 1)) - per_slice = segment.as_bool(data.get("per_slice", False)) + # As the browser reads them (label_it.seed_options): a refusal is a ValueError, 400. + options = label_it.seed_options(data) + method = options["method"] volume_id, volume, lo, hi = _target_box(data) refused = _confirmation_needed(data, lo, hi) if refused: @@ -615,7 +545,7 @@ def seed_view_from_prediction(): ) objects = segment_prediction( method, prediction, info.get("output_class"), channels, offsets, - 0.5 if threshold is None else float(threshold), min_size, connectivity, per_slice, + options["threshold"], options["min_size"], options["connectivity"], options["per_slice"], ) # An instance target needs ids apart from its neighbours'; a selected # channel trains as a binary target. @@ -627,7 +557,7 @@ def labels_for(existing): return fill.seed_labels(objects, existing, count_up=instance_target and existing.dtype.itemsize > 1) answer = _fill(volume_id, volume, lo, hi, labels_for) - return jsonify({**answer, "model": model_name, "threshold": threshold, "method": method}) + return jsonify({**answer, "model": model_name, "threshold": data.get("threshold"), "method": method}) except _Refused as e: return _error(str(e), e.status, **e.extra) except FileNotFoundError as e: diff --git a/cellmap_flow/finetune/data/sampler.py b/cellmap_flow/finetune/data/sampler.py index bc3b0103..1d9e3a49 100644 --- a/cellmap_flow/finetune/data/sampler.py +++ b/cellmap_flow/finetune/data/sampler.py @@ -45,6 +45,7 @@ from cellmap_flow.finetune.session.manifest import voxels_inside_any_bbox from cellmap_flow.finetune.session.volume import volume_corner_nm from cellmap_flow.io.geometry import list_populated_chunks +from cellmap_flow.portable.regions import region_centre_voxels logger = logging.getLogger(__name__) @@ -448,17 +449,18 @@ def _index_rehearsal(self) -> None: """ centres = [] for region in self.good_regions: - try: - offset_nm = np.array(region["offset_nm"], dtype=float) - shape_nm = np.array(region["shape_nm"], dtype=float) - except (KeyError, TypeError, ValueError): + # What counts as a region, and where its centre lies, is the rule + # of cellmap_flow.portable.regions, by which the browser's + # trainer places regions too. + where, centre_voxels = region_centre_voxels( + region, self.corner_nm, self.output_voxel_size, self.shape_voxels + ) + if where == "malformed": logger.warning(f"Skipping malformed good region: {region!r}") continue - centre_nm = offset_nm + shape_nm / 2.0 - centre_voxels = (centre_nm - self.corner_nm) / self.output_voxel_size # A region marked against a different volume would sample pure # out-of-bounds zeros and quietly anchor the model to nothing. - if np.any(centre_voxels < 0) or np.any(centre_voxels >= self.shape_voxels): + if where == "outside": logger.warning( f"Good region {region.get('id', '?')} centres outside the " f"volume at voxel {centre_voxels.astype(int).tolist()}; skipping." diff --git a/cellmap_flow/finetune/session/fill.py b/cellmap_flow/finetune/session/fill.py index 8c0e7c09..a50403ad 100644 --- a/cellmap_flow/finetune/session/fill.py +++ b/cellmap_flow/finetune/session/fill.py @@ -16,6 +16,11 @@ the brush paints cuts an object under it, where under 26-connectivity the voxels across a diagonal wall would still touch. +The decisions (which voxels a box covers, the ids a seed or a split gives, +which voxels each write changes and an undo puts back) are +``cellmap_flow.portable.fill``'s, which the browser makes too, over its paint +store; they are re-exported here, beside the reads and writes around them. + The write goes to MinIO, as cc3d_relabel's does, not to the volume on disk: MinIO holds strokes the periodic sync has not pulled yet, which is what "only fill 0s" must see, and it is what neuroglancer reads. The caller then pulls @@ -31,144 +36,18 @@ import zarr from cellmap_flow.finetune.session import minio -from cellmap_flow.finetune.session.volume import volume_corner_nm +from cellmap_flow.portable.fill import ( # noqa: F401 (re-exported: the routes label boxes through them) + box_voxels, + paint, + relabel_objects, + restore, + rewrite, + seed_labels, +) logger = logging.getLogger(__name__) -def box_voxels(volume, offset_nm, shape_nm, volume_shape): - """``(lo, hi)``: the annotation voxels whose centres lie in a box, clipped to the volume. - - ``offset_nm`` and ``shape_nm`` are the box's lower corner and size in - world nm, as a good region records them; ``volume`` is the volume's - record, for its grid. None when no voxel of the volume is in the box. - """ - voxel_size = np.asarray(volume["output_voxel_size"], dtype=float) - corner = volume_corner_nm(volume.get("dataset_offset_nm"), voxel_size) - start = (np.asarray(offset_nm, dtype=float) - corner) / voxel_size - end = start + np.asarray(shape_nm, dtype=float) / voxel_size - # Voxel i's centre is at i + 0.5 voxels from the corner. - lo = np.maximum(np.ceil(start - 0.5), 0).astype(int) - hi = np.minimum(np.ceil(end - 0.5), np.asarray(volume_shape)).astype(int) - if np.any(hi <= lo): - return None - return lo, hi - - -def _fresh_ids(existing, count, count_up): - """``count`` ids for new objects in a box that holds ``existing``. - - ``count_up``: from one past the box's largest label, so an affinity - target's objects stay apart from every object nearby (ids elsewhere in - the volume are not looked at: affinities only compare nearby voxels). - Else the lowest ids the box does not use, so a uint8 volume's 254 ids - are never run through: a repeat in another box is only a repeated - colour, which a binary or distance target does not see. Raises - ValueError when the dtype has no room. - """ - biggest = np.iinfo(existing.dtype).max - if count_up: - first = max(int(existing.max()), 1) + 1 - ids = np.arange(first, first + count) - else: - used = np.unique(existing[existing >= 2]).astype(np.int64) - candidates = np.arange(2, min(biggest, len(used) + count + 2) + 1) - ids = np.setdiff1d(candidates, used, assume_unique=True)[:count] - if ids.size < count or (ids.size and ids[-1] > biggest): - raise ValueError( - f"{count} new objects do not fit in the volume's {existing.dtype} labels: make a new annotation " - "volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min " - "size or the threshold so the seed makes fewer objects" - ) - return ids - - -def seed_labels(objects, existing, count_up=False): - """Labels for a box from a segmentation: an id per object inside, 1 outside. - - ``objects`` is a ``post.segment`` segmenter's output over the box (0 - background, objects 1..n), ``existing`` what the box holds now. Each - object gets an id of its own (``_fresh_ids``; ``count_up`` for an - instance target), so neighbouring objects are told apart, and a merge - the model made shows as one colour to split; but an object the user has - already painted part of takes the id painted there most, or the - unpainted rest of it would be taught as a different object from its - painted part. A binary or distance target reads every id as foreground, - so the ids cost it nothing. - """ - objects = np.asarray(objects).astype(np.int64, copy=False) - foreground = objects > 0 - labels = np.where(foreground, 2, 1).astype(existing.dtype) - if not foreground.any(): - return labels - n = int(objects.max()) - ids = np.zeros(n + 1, dtype=np.int64) - painted = (existing >= 2) & foreground - if painted.any(): - pairs, counts = np.unique( - np.stack([objects[painted], existing[painted].astype(np.int64)]), axis=1, return_counts=True - ) - # Most-painted last, so its id is the one that stays. - order = np.argsort(counts, kind="stable") - ids[pairs[0, order]] = pairs[1, order] - present = np.zeros(n + 1, dtype=bool) - present[np.unique(objects[foreground])] = True - fresh = np.flatnonzero(present & (ids == 0)) - ids[fresh] = _fresh_ids(existing, fresh.size, count_up) - labels[foreground] = ids[objects[foreground]].astype(existing.dtype) - return labels - - -def relabel_objects(existing, count_up=False, connectivity=1, per_slice=False): - """The box's foreground (labels 2 and up) relabelled by connected component. - - Each component takes the id most of its voxels hold, so a stroke that - joins two objects merges them into the id of the bigger. An id left on - several components, an object a background wall cut in two, stays on the - largest and the others get fresh ids (``_fresh_ids``). Background (1) - and unpainted (0) voxels are left alone. Returns ``(labels, counts)``: - ``counts`` has ``objects``, ``split`` (components given a fresh id) and - ``merged`` (components that held more than one id). - - A cut has to go through every slice the object spans: the brush paints - one slice, and an object cut in one slice is still one object above and - below it. ``split`` is 0 then, which the page says. With ``per_slice`` - each z slice's pieces are components on their own, so a wall in one - slice splits that slice, and every slice of an object gets its own id. - ``connectivity``: as for ``post.segment.connected_components``. - """ - from cellmap_flow.post.segment import connected_components - - labels = existing.copy() - foreground = existing >= 2 - counts = {"objects": 0, "split": 0, "merged": 0} - if not foreground.any(): - return labels, counts - components = connected_components(foreground, connectivity, per_slice=per_slice).astype(np.int64) - n = int(components.max()) - comp, old = components[foreground], existing[foreground].astype(np.int64) - pairs, pair_counts = np.unique(np.stack([comp, old]), axis=1, return_counts=True) - order = np.argsort(pair_counts, kind="stable") - majority = np.zeros(n + 1, dtype=np.int64) - majority[pairs[0, order]] = pairs[1, order] # the most voxels last, so it stays - counts["objects"] = int(n) - counts["merged"] = int(np.count_nonzero(np.bincount(pairs[0], minlength=n + 1) > 1)) - - sizes = np.bincount(comp, minlength=n + 1) - fresh = [] - for old_id in np.unique(majority[1:]): - claimants = np.flatnonzero(majority == old_id) - if claimants.size > 1: - keep = claimants[np.argmax(sizes[claimants])] - fresh.extend(int(c) for c in claimants if c != keep) - new = majority.copy() - if fresh: - new[fresh] = _fresh_ids(existing, len(fresh), count_up) - counts["split"] = len(fresh) - labels[foreground] = new[comp].astype(existing.dtype) - return labels, counts - - def open_served_labels(state, volume_id): """The volume's ``annotation/s0`` as MinIO serves it, open for writing. @@ -222,20 +101,12 @@ def paint_box(state, volume_id, lo, hi, labels_for, local_zarr_path=None, undo=N _upload_chunks_only_on_disk(s3, root, arr, lo, hi, local_zarr_path) box = tuple(slice(int(a), int(b)) for a, b in zip(lo, hi)) existing = arr[box] - labels = np.asarray(labels_for(existing), dtype=arr.dtype) - if overwrite: - write = (labels > 0) & (labels != existing) - else: - write = (existing == 0) & (labels > 0) - n_overwritten = int(np.count_nonzero(write & (existing != 0))) - if write.any(): - before = existing.copy() - existing[write] = labels[write] - arr[box] = existing + after, counts = paint(existing, labels_for(existing), overwrite=overwrite) + if after is not None: + arr[box] = after if undo is not None: - undo.append((np.asarray(lo), np.asarray(hi), before, existing.copy())) - n_foreground = int(np.count_nonzero(write & (labels >= 2))) - return n_foreground, int(np.count_nonzero(write)) - n_foreground, n_overwritten + undo.append((np.asarray(lo), np.asarray(hi), existing, after.copy())) + return counts def fill_unpainted(state, volume_id, lo, hi, labels_for, local_zarr_path=None, undo=None): @@ -268,14 +139,12 @@ def rewrite_foreground(state, volume_id, lo, hi, relabel, local_zarr_path=None, box = tuple(slice(int(a), int(b)) for a, b in zip(lo, hi)) existing = arr[box] labels, counts = relabel(existing) - changed = (existing >= 2) & (np.asarray(labels, dtype=arr.dtype) != existing) - if changed.any(): - before = existing.copy() - existing[changed] = np.asarray(labels, dtype=arr.dtype)[changed] - arr[box] = existing + after, n_changed = rewrite(existing, labels) + if after is not None: + arr[box] = after if undo is not None: - undo.append((np.asarray(lo), np.asarray(hi), before, existing.copy())) - return int(np.count_nonzero(changed)), counts + undo.append((np.asarray(lo), np.asarray(hi), existing, after.copy())) + return n_changed, counts def restore_box(state, volume_id, lo, hi, before, after): @@ -287,9 +156,7 @@ def restore_box(state, volume_id, lo, hi, before, after): """ _, _, arr = open_served_labels(state, volume_id) box = tuple(slice(int(a), int(b)) for a, b in zip(lo, hi)) - current = arr[box] - restore = (before != after) & (current == after) - if restore.any(): - current[restore] = before[restore] - arr[box] = current - return int(np.count_nonzero(restore)) + restored, n = restore(arr[box], before, after) + if restored is not None: + arr[box] = restored + return n diff --git a/cellmap_flow/finetune/session/volume.py b/cellmap_flow/finetune/session/volume.py index c1af0ab6..c498df4d 100644 --- a/cellmap_flow/finetune/session/volume.py +++ b/cellmap_flow/finetune/session/volume.py @@ -12,6 +12,10 @@ The root attr ``dataset_offset_nm`` is voxel 0's *centre* and is also the OME translation, since that is where Neuroglancer draws voxel 0 while it is painted; ``volume_corner_nm`` gives the corner. + +Where a new volume lies (``plan_volume``) is planned by +``cellmap_flow.portable.volume`` on the numbers read here, as the browser +plans one on its own reads; this module reads and writes the zarrs. """ import logging @@ -27,7 +31,10 @@ import zarr from cellmap_flow.io.multiscale import closest_raw_scale -from cellmap_flow.io.ome import ome_corner, ome_translation +from cellmap_flow.portable import volume as portable_volume +# Voxel 0's corner from a volume's dataset_offset_nm, its centre; imported +# from here by the server's readers of volumes. +from cellmap_flow.portable.volume import volume_corner_nm # noqa: F401 logger = logging.getLogger(__name__) @@ -100,40 +107,35 @@ def new_volume_id() -> str: return f"vol-{uuid.uuid4().hex[:8]}-{datetime.now().strftime('%Y%m%d-%H%M%S')}" -def volume_corner_nm(dataset_offset_nm, output_voxel_size) -> np.ndarray: - """The world position of an annotation volume's voxel-0 lower corner, in nm. +def raw_level(raw_dataset_path: str, input_voxel_size, resample: bool = False) -> dict: + """The raw level a model at ``input_voxel_size`` reads, as the numbers + ``cellmap_flow.portable.volume.plan_volume`` plans on: ``voxel`` and + ``corner`` in nm and ``shape`` in voxels, z, y, x. - ``dataset_offset_nm`` (a root attr of every volume) is also written as the - volume's OME-NGFF translation, and a translation is voxel 0's *centre*. - Neuroglancer drew the volume that way while it was painted, so that is - where the labels are: read as a corner, the value puts every label half - an annotation voxel away from where it was drawn. + The level ``select_level``'s "floor" picks (``closest_raw_scale``), at its + own voxel size; with ``resample``, the raw resampled to + ``input_voxel_size``, as the trainer and a --resample server read it. """ - offset = np.zeros(3) if dataset_offset_nm is None else dataset_offset_nm - return np.asarray(ome_corner(offset, output_voxel_size), dtype=float) - - -def _grid(raw_dataset_path, output_voxel_size, chunk_size, resample=False): - """``(offset_nm, shape_voxels)`` of a volume over the data, padded to whole chunks.""" from cellmap_flow.image_data_interface import ImageDataInterface - output_voxel_size = np.asarray(output_voxel_size, dtype=float) - chunk_size = np.asarray(chunk_size, dtype=int) - idi = ImageDataInterface( - raw_dataset_path, - voxel_size=output_voxel_size, - on_voxel_size_mismatch="resample" if resample else "relabel", - ) - offset = np.asarray(ome_translation(np.asarray(idi.offset, dtype=float), output_voxel_size)) - # The data's own extent in output voxels, rounded up so a partial voxel - # at the far end is covered. Not idi.roi's: that is the whole-nm box - # around the data, up to 2 nm larger, which would add a voxel to every - # level whose extent is not whole nm (10.48 nm...). - extent = np.asarray(idi.shape, dtype=float)[-output_voxel_size.size:] * np.asarray( - idi.voxel_size, dtype=float - ) - shape = np.ceil(np.round(extent / output_voxel_size, 6)).astype(int) - return offset, np.ceil(shape / chunk_size).astype(int) * chunk_size + if resample: + voxel_size, mismatch = input_voxel_size, "resample" + else: + # Opened at the level's own voxel size, so that it is read as it is + # stored and its corner is its own, not one relabelled to the model's. + voxel_size = closest_raw_scale(raw_dataset_path, tuple(input_voxel_size)) or input_voxel_size + mismatch = "relabel" + idi = ImageDataInterface(raw_dataset_path, voxel_size=voxel_size, on_voxel_size_mismatch=mismatch) + axes = len(input_voxel_size) + # The data's own extent is its shape times its voxel size, which the plan + # takes. Not idi.roi's: that is the whole-nm box around the data, up to + # 2 nm larger, which would add a voxel to every level whose extent is not + # whole nm (10.48 nm...). + return { + "voxel": [float(v) for v in idi.voxel_size][-axes:], + "corner": [float(o) for o in idi.offset][-axes:], + "shape": [int(n) for n in idi.shape][-axes:], + } def plan_volume(raw_dataset_path: str, model_geometry, resample: bool = False) -> VolumeGeometry: @@ -142,40 +144,19 @@ def plan_volume(raw_dataset_path: str, model_geometry, resample: bool = False) - ``model_geometry`` has ``input_voxel_size`` and ``output_voxel_size``, and either ``input_shape``/``output_shape`` in voxels or ``read_shape``/``write_shape`` in nm, which is what a server reports. - Each voxel size is snapped to the raw level closest to it (the model's - own is kept as the claimed one); the volume lies on the output level's - grid from its corner, one chunk per model output, and covers the data - padded to whole chunks. - - With ``resample`` (the dashboard's Resample setting) nothing is snapped: - the volume is at the model's own voxel sizes, and the trainer reads the - raw resampled to them, as a server started with --resample serves it. + The planning is ``cellmap_flow.portable.volume.plan_volume``'s, which the + browser runs too: the volume lies on the grid the model's prediction is + served on, that of the raw level it reads (``raw_level``), one chunk per + model output, and covers the level padded to whole chunks; the model's + own voxel sizes are kept as the claimed ones. + + With ``resample`` (the dashboard's Resample setting) the level is the raw + resampled to the model's input voxel size, so the volume is at the + model's own voxel sizes, and the trainer reads the raw resampled to them, + as a server started with --resample serves it. """ - claimed_in = np.array(model_geometry.input_voxel_size) - claimed_out = np.array(model_geometry.output_voxel_size) - if getattr(model_geometry, "input_shape", None) is not None: - input_size = np.array(model_geometry.input_shape, dtype=int) - output_size = np.array(model_geometry.output_shape, dtype=int) - else: - input_size = (np.array(model_geometry.read_shape) / claimed_in).astype(int) - output_size = (np.array(model_geometry.write_shape) / claimed_out).astype(int) - if resample: - output_voxel_size, input_voxel_size = claimed_out, claimed_in - else: - output_voxel_size = np.array(closest_raw_scale(raw_dataset_path, tuple(claimed_out)) or claimed_out) - input_voxel_size = np.array(closest_raw_scale(raw_dataset_path, tuple(claimed_in)) or claimed_in) - offset, shape = _grid(raw_dataset_path, output_voxel_size, output_size, resample) - return VolumeGeometry( - output_voxel_size=output_voxel_size, - input_voxel_size=input_voxel_size, - claimed_output_voxel_size=claimed_out, - claimed_input_voxel_size=claimed_in, - chunk_size=output_size, - input_size=input_size, - dataset_offset_nm=offset, - dataset_shape_voxels=shape, - resample=bool(resample), - ) + level = raw_level(raw_dataset_path, model_geometry.input_voxel_size, resample) + return VolumeGeometry(**portable_volume.plan_volume(level, model_geometry), resample=bool(resample)) def create_volume_zarr( diff --git a/cellmap_flow/finetune/target_transforms.py b/cellmap_flow/finetune/target_transforms.py index 5668d4d8..cb9d7bcf 100644 --- a/cellmap_flow/finetune/target_transforms.py +++ b/cellmap_flow/finetune/target_transforms.py @@ -14,6 +14,11 @@ except IntervalTargetTransform, whose "target" is a lower and an upper bound on a distance model's output, for losses.IntervalLoss. +The maths is ``cellmap_flow/portable/targets.py``, numpy functions the +browser runs too (in chunkmirage's Pyodide worker, on one painted chunk); +the classes here hold a transform's settings and move the trainer's tensors +to and from them, so both modes make the same targets from the same paint. + A flow model's target (Cellpose's: flows towards each instance's centre and a foreground channel, with a mask per channel) is made by ``instance_flows.FlowTargetTransform``, beside the loss it goes with. @@ -22,9 +27,13 @@ import logging from typing import List, Optional, Tuple +import numpy as np import torch from torch import Tensor +from cellmap_flow.portable import targets +from cellmap_flow.portable.targets import SATURATION_SIGMAS # noqa: F401 (losses.py reads it here) + logger = logging.getLogger(__name__) @@ -52,6 +61,19 @@ def read_offsets_from_script(script_path) -> Optional[list]: return None +def _labels(annotation: Tensor) -> np.ndarray: + """The annotation's labels as numpy, (B, Z, Y, X): the batch with its one channel taken out.""" + ann = annotation.detach().cpu().numpy() + if ann.ndim != 5 or ann.shape[1] != 1: + raise ValueError(f"Expected annotation of shape (B, 1, Z, Y, X), got {tuple(ann.shape)}") + return ann[:, 0] + + +def _tensors(annotation: Tensor, *arrays: np.ndarray) -> Tuple[Tensor, ...]: + """The portable function's arrays as tensors on the annotation's device.""" + return tuple(torch.from_numpy(a).to(annotation.device) for a in arrays) + + class TargetTransform: """Base class for target transforms.""" @@ -64,14 +86,11 @@ class BinaryTargetTransform(TargetTransform): """Standard binary segmentation transform (current default behavior). Produces single-channel binary target: bg=0, fg=1. - Mask marks annotated regions. + Mask marks annotated regions (``targets.binary``). """ def __call__(self, annotation: Tensor) -> Tuple[Tensor, Tensor]: - mask = (annotation > 0).float() - target = torch.clamp(annotation - 1, min=0) - target = (target > 0).float() - return target, mask + return _tensors(annotation, *targets.binary(_labels(annotation))) class BroadcastBinaryTargetTransform(TargetTransform): @@ -79,30 +98,27 @@ class BroadcastBinaryTargetTransform(TargetTransform): All output channels receive the same fg/bg target. Useful for treating multi-channel models (affinities, distances) - as simple binary segmentation. + as simple binary segmentation (``targets.binary`` on N channels). """ def __init__(self, num_channels: int): self.num_channels = num_channels def __call__(self, annotation: Tensor) -> Tuple[Tensor, Tensor]: - mask = (annotation > 0).float() - target = (torch.clamp(annotation - 1, min=0) > 0).float() - # expand is lazy (no memory copy), contiguous() ensures safe downstream use - target = target.expand(-1, self.num_channels, -1, -1, -1).contiguous() - mask = mask.expand(-1, self.num_channels, -1, -1, -1).contiguous() - return target, mask + return _tensors(annotation, *targets.binary(_labels(annotation), self.num_channels)) class AffinityTargetTransform(TargetTransform): - """Compute affinity targets from instance labels. + """Compute affinity targets from instance labels (``targets.affinity``). For each offset, affinity is: 1 if both voxels belong to the same foreground object (same label > 1) 0 if different objects, or either is background The loss mask requires both voxels in each pair to be annotated (label > 0), - producing a per-channel mask since each offset shifts differently. + producing a per-channel mask since each offset shifts differently. An + offset as long as the patch along its axis or longer pairs no voxel, and + its channel's mask is empty. Args: offsets: List of [dz, dy, dx] offset tuples defining neighbor relationships. @@ -117,76 +133,19 @@ def __init__(self, offsets: List[List[int]], num_channels: int = None): self.num_channels = num_channels if num_channels is not None else len(offsets) def __call__(self, annotation: Tensor) -> Tuple[Tensor, Tensor]: - B, _C, Z, Y, X = annotation.shape - # Allocate for all output channels; non-affinity channels stay zero (masked out) - target = torch.zeros(B, self.num_channels, Z, Y, X, device=annotation.device) - mask = torch.zeros(B, self.num_channels, Z, Y, X, device=annotation.device) - - labels = annotation[:, 0] # (B, Z, Y, X) - annotated = labels > 0 # bool - - for i, offset in enumerate(self.offsets): - dz, dy, dx = offset - src_slices, dst_slices = _offset_slices(Z, Y, X, dz, dy, dx) - - src_labels = labels[(slice(None), *src_slices)] - dst_labels = labels[(slice(None), *dst_slices)] - src_ann = annotated[(slice(None), *src_slices)] - dst_ann = annotated[(slice(None), *dst_slices)] - - # Affinity = 1 iff same foreground object - same_fg = (src_labels == dst_labels) & (src_labels > 1) - both_annotated = src_ann & dst_ann - - target[(slice(None), i, *src_slices)] = same_fg.float() - mask[(slice(None), i, *src_slices)] = both_annotated.float() - - return target, mask - - -def _offset_slices(Z, Y, X, dz, dy, dx): - """Compute source and destination slices for an offset. - - For a volume of shape (Z, Y, X) and offset (dz, dy, dx), - returns slices such that: - volume[src_slices] and volume[dst_slices] - are aligned views offset by (dz, dy, dx). - """ - - def _dim_slices(size, d): - if d > 0: - return slice(None, size - d), slice(d, None) - elif d < 0: - return slice(-d, None), slice(None, size + d) - else: - return slice(None), slice(None) - - sz, dz_s = _dim_slices(Z, dz) - sy, dy_s = _dim_slices(Y, dy) - sx, dx_s = _dim_slices(X, dx) - - return (sz, sy, sx), (dz_s, dy_s, dx_s) + return _tensors(annotation, *targets.affinity(_labels(annotation), self.offsets, self.num_channels)) class DistanceTargetTransform(TargetTransform): - """Soft signed-distance target for models trained the fly_organelles way. + """Soft signed-distance target for models trained the fly_organelles way + (``targets.distance``, which says how it is made and masked). The target is ``(tanh((edt(fg) - edt(not fg)) / sigma) + 1) / 2`` with - distances in voxels: 0.5 on the object boundary, rising to 1 inside and - falling to 0 outside, saturating a few sigma away. That is exactly what - ``fly_organelles.utils.Distance`` fed the cellmap distance models (the - nuc/mito "*_distance_*" HuggingFace repos use sigma=6), so sigmoid(model - output) is comparable to this target voxel for voxel. Use it with a - BCE-with-logits loss, which accepts soft targets; margin and dice do not. - - Masking: an EDT computed inside a patch only sees boundaries inside the - patch. For an annotated voxel the computed |d| is an upper bound on the - truth -- the real nearest boundary may sit just past the patch edge or - inside an unannotated pocket. A voxel is supervised only when its - computed |d| is no larger than its distance to the nearest unannotated - voxel or patch edge (so no unseen boundary can be closer), or when that - distance is already past 3*sigma, where tanh has saturated and the exact - value no longer matters. + distances in voxels, 0.5 on the object boundary, so sigmoid(model output) + is comparable to it voxel for voxel. Use it with a BCE-with-logits loss, + which accepts soft targets; margin and dice do not. A voxel is + supervised only where no boundary unseen in the patch can be nearer than + the one it measures, or where tanh has saturated anyway. Args: sigma_voxels: tanh scale in output voxels (6 for the cellmap @@ -201,103 +160,20 @@ def __init__(self, sigma_voxels: float = 6.0, num_channels: int = 1): self.sigma = float(sigma_voxels) self.num_channels = int(num_channels) - def _one(self, ann): - """(Z, Y, X) uint annotation -> (target, mask) float32 numpy arrays.""" - import numpy as np - from scipy.ndimage import distance_transform_edt as edt - - annotated = ann > 0 - fg = ann >= 2 - bg = ann == 1 - target = np.zeros(ann.shape, dtype=np.float32) - mask = np.zeros(ann.shape, dtype=np.float32) - if not annotated.any(): - return target, mask - - # edt(x) is the distance from each nonzero voxel of x to the nearest - # zero. With no zero anywhere scipy returns a large finite number for - # every voxel; treat that as "no boundary in this patch" explicitly. - # - # Both sides measure to the nearest *known* voxel of the other class: - # a background voxel's distance to annotated foreground, and a - # foreground voxel's distance to annotated background. d_in used to - # be edt(fg), which counts unannotated voxels as background too, so a - # foreground voxel next to an unannotated one -- at the edge of a - # dense crop that cuts through an object, say -- got a small d_in that - # always passed the trust test below, and was supervised toward 0.58 - # to 0.88 as though the object ended at the crop edge. - d_in = edt(~bg) if bg.any() else np.full(ann.shape, np.inf) - d_out = edt(~fg) if fg.any() else np.full(ann.shape, np.inf) - signed = np.where(fg, d_in, -d_out) - - # Distance from each annotated voxel to the nearest unannotated voxel - # or to just outside the patch (the one-voxel pad of False). - known = np.pad(annotated, 1, constant_values=False) - trust = edt(known)[1:-1, 1:-1, 1:-1] - - reliable = np.abs(signed) <= trust - saturated = trust >= 3.0 * self.sigma - mask[annotated & (reliable | saturated)] = 1.0 - target[:] = (np.tanh(signed / self.sigma) + 1.0) / 2.0 - return target, mask - def __call__(self, annotation: Tensor) -> Tuple[Tensor, Tensor]: - import numpy as np - - ann = annotation.detach().cpu().numpy() - if ann.ndim != 5 or ann.shape[1] != 1: - raise ValueError( - f"Expected annotation of shape (B, 1, Z, Y, X), got {tuple(ann.shape)}" - ) - targets = np.empty(ann.shape, dtype=np.float32) - masks = np.empty(ann.shape, dtype=np.float32) - for b in range(ann.shape[0]): - targets[b, 0], masks[b, 0] = self._one(ann[b, 0]) - target = torch.from_numpy(targets).to(annotation.device) - mask = torch.from_numpy(masks).to(annotation.device) - if self.num_channels > 1: - target = target.expand(-1, self.num_channels, -1, -1, -1).contiguous() - mask = mask.expand(-1, self.num_channels, -1, -1, -1).contiguous() - return target, mask - - -# How far out, in sigma, a distance bound still says anything: tanh(3) is -# 0.995, so a voxel known to be at least 3 sigma inside is as good as at 3 -# sigma, and an upper bound beyond it constrains nothing a threshold sees. -SATURATION_SIGMAS = 3.0 + return _tensors(annotation, *targets.distance(_labels(annotation), self.sigma, self.num_channels)) class IntervalTargetTransform(TargetTransform): - """Bounds on a distance model's output at each painted voxel, for scribbles. - - A distance target needs every voxel's distance to the object boundary, - and a stroke does not say where the boundary is. It does bound it: - - - upper, U: the nearest voxel painted as the other class. The boundary - lies between the two, so |d| <= U. - - lower, L: the nearest voxel not painted as the voxel's own class -- - the other class, unpainted (it may be either), or past the patch edge. - Every voxel closer is known to be its own class, so |d| >= L. - - Where the paint is dense, the nearest voxel not of its own class is of - the other, and L == U: the exact distance DistanceTargetTransform would - give, with the same convention (edt to the nearest voxel of the other - class, so 1 voxel on either side of a boundary). On a stroke the two - bracket the truth. Both are computed in the patch the trainer gets, which - keeps them valid: a closer other-class voxel outside the patch would only - lower the true |d|, which U already exceeds or meets, and the patch edge - counts as "not its own class", which can only lower L. Reading context - around the patch would tighten bounds near its edges, but the dataset - hands the trainer the patch alone (augmented with it), so they are left - as loose as the patch makes them. - - Distances are Euclidean in nm (``voxel_size_nm``, so an anisotropic - grid is measured as it is), and sigma, which is in output voxels as for - the distance target, is taken in the finest axis's voxels. The bounds - are returned in the model's logit space, where the distance target - (tanh(d/sigma) + 1)/2 = sigmoid(2d/sigma) is the line z = 2d/sigma, with - the sign of the painted class. Past SATURATION_SIGMAS a lower bound is - capped there and an upper bound dropped (+-inf). + """Bounds on a distance model's output at each painted voxel, for scribbles + (``targets.interval``, with ``targets.distance_bounds``, says how). + + A stroke does not say where the object boundary is, but it bounds it: + no further than the nearest voxel painted as the other class, no nearer + than the nearest voxel not painted as the voxel's own. Where the paint is + dense the two meet at the distance DistanceTargetTransform would give. + Distances are Euclidean in nm (``voxel_size_nm``), sigma is in the finest + axis's output voxels, and the bounds are in the model's logit space. Returns ``(bounds, mask)``: bounds (B, 2, Z, Y, X), the lower and upper bound on the logit (0 off the paint), and mask (B, 1, Z, Y, X), the @@ -310,55 +186,12 @@ def __init__(self, sigma_voxels: float = 6.0, voxel_size_nm=None): raise ValueError(f"sigma_voxels must be positive, got {sigma_voxels}") voxel_size = (1.0, 1.0, 1.0) if voxel_size_nm is None else voxel_size_nm self.voxel_size_nm = tuple(float(v) for v in voxel_size) - self.sigma_nm = float(sigma_voxels) * min(self.voxel_size_nm) + self.sigma_voxels = float(sigma_voxels) + self.sigma_nm = targets.sigma_nm(sigma_voxels, self.voxel_size_nm) def distance_bounds(self, ann): """(L, U) in nm for each voxel of a (Z, Y, X) annotation; 0 off the paint, U inf with no other class.""" - import numpy as np - from scipy.ndimage import distance_transform_edt as edt - - sampling = self.voxel_size_nm - fg, bg = ann >= 2, ann == 1 - lower = np.zeros(ann.shape) - upper = np.zeros(ann.shape) - for own, other in ((fg, bg), (bg, fg)): - if not own.any(): - continue - # edt(x) is the distance from each nonzero voxel of x to the - # nearest zero; with no zero at all scipy returns garbage, so a - # patch without the other class has no upper bound, explicitly. - to_other = edt(~other, sampling=sampling) if other.any() else np.inf - # Padded with a ring of "not own class": the patch's surroundings. - to_not_own = edt(np.pad(own, 1, constant_values=False), sampling=sampling)[1:-1, 1:-1, 1:-1] - upper[own] = np.broadcast_to(to_other, ann.shape)[own] - lower[own] = to_not_own[own] - return lower, upper - - def _one(self, ann): - """(Z, Y, X) annotation -> (lower, upper, mask) float32 numpy arrays, the bounds in logits.""" - import numpy as np - - lower, upper = self.distance_bounds(ann) - logit_per_nm = 2.0 / self.sigma_nm - saturation = SATURATION_SIGMAS * self.sigma_nm - near = logit_per_nm * np.minimum(lower, saturation) - far = np.where(upper < saturation, logit_per_nm * upper, np.inf) - fg, bg = ann >= 2, ann == 1 - # Background lies on the negative side: its bounds swap and change sign. - lo = np.where(fg, near, np.where(bg, -far, 0.0)) - hi = np.where(fg, far, np.where(bg, -near, 0.0)) - return lo.astype(np.float32), hi.astype(np.float32), (ann > 0).astype(np.float32) + return targets.distance_bounds(ann, self.voxel_size_nm) def __call__(self, annotation: Tensor) -> Tuple[Tensor, Tensor]: - import numpy as np - - ann = annotation.detach().cpu().numpy() - if ann.ndim != 5 or ann.shape[1] != 1: - raise ValueError( - f"Expected annotation of shape (B, 1, Z, Y, X), got {tuple(ann.shape)}" - ) - bounds = np.empty((ann.shape[0], 2, *ann.shape[2:]), dtype=np.float32) - masks = np.empty(ann.shape, dtype=np.float32) - for b in range(ann.shape[0]): - bounds[b, 0], bounds[b, 1], masks[b, 0] = self._one(ann[b, 0]) - return torch.from_numpy(bounds).to(annotation.device), torch.from_numpy(masks).to(annotation.device) + return _tensors(annotation, *targets.interval(_labels(annotation), self.sigma_voxels, self.voxel_size_nm)) diff --git a/cellmap_flow/models/configs/cellmap.py b/cellmap_flow/models/configs/cellmap.py index 9f904892..02452dfd 100644 --- a/cellmap_flow/models/configs/cellmap.py +++ b/cellmap_flow/models/configs/cellmap.py @@ -4,10 +4,39 @@ model is its TorchScript export. """ +import logging + from funlib.geometry import Coordinate from cellmap_flow.models.configs.base import Config, ModelConfig, _get_device +logger = logging.getLogger(__name__) + + +def runs_at(model, input_shape, output_shape) -> bool: + """Whether the TorchScript ``model`` runs on an input of ``input_shape`` + voxels and answers ``output_shape``. + + cellmap_models traces the export (``torch.jit.trace``) at whichever shape + the exporter passed, and a traced UNet's crops are baked in, so it runs + only at that shape: fed another, it either fails (jrc_mus-livers_16nm_to_8nm_mito, + traced at its inference 288³, fails at its training 216³ with "padded + input size (1 x 1 x 1)") or answers the baked-in output + (mito_aff_unet_setup_16, traced at its training 178³ → 56³, answers 56³ + to a 378³ input). The metadata records both shapes and not which one + the export was traced at, so this asks the model. + """ + import torch + + try: + first = next(model.parameters(), None) + device = first.device if first is not None else "cpu" + with torch.no_grad(): + out = model(torch.zeros((1, 1, *[int(s) for s in input_shape]), device=device)) + except (RuntimeError, TypeError, ValueError): + return False + return tuple(int(s) for s in out.shape[-len(output_shape):]) == tuple(int(s) for s in output_shape) + class CellMapModelConfig(ModelConfig): """Configuration class for a CellmapModel.""" @@ -44,24 +73,29 @@ def _get_config(self) -> Config: config.channels_names = metadata.channels_names config.channels = metadata.channels_names # alias for compatibility - # The export's inference shapes when the metadata has them, the - # training shapes otherwise. cellmap_models traces the TorchScript at - # the inference shape (torch.jit.trace), and a traced UNet runs only - # at that shape: jrc_mus-livers_16nm_to_8nm_mito fails at its training - # 216³ ("padded input size (1 x 1 x 1)") and runs at its inference - # 288³, which is also what the Pipeline builder shows for it. - input_shape = getattr(metadata, "inference_input_shape", None) or metadata.input_shape - output_shape = getattr(metadata, "inference_output_shape", None) or metadata.output_shape + config.model = self.cellmap_model.ts_model + config.model.to(_get_device()) + config.model.eval() + + # The shape the export runs at: its training shapes (input_shape → + # output_shape) if a forward at them answers the training output, + # else its inference shapes (what the Pipeline builder shows). The + # metadata carries both; which the TorchScript was traced at, only + # the model can say (runs_at). The probe at the training shape is the + # cheap one: the smaller input, and a mismatch fails fast. + training = (list(metadata.input_shape), list(metadata.output_shape)) + inference = (getattr(metadata, "inference_input_shape", None), getattr(metadata, "inference_output_shape", None)) + if inference[0] and inference[1] and list(inference[0]) != training[0] and not runs_at(config.model, *training): + input_shape, output_shape = list(inference[0]), list(inference[1]) + logger.info(f"{self.name} runs at its inference shape {input_shape} → {output_shape}, not its training shape {training[0]}") + else: + input_shape, output_shape = training config.read_shape = Coordinate(input_shape) * config.input_voxel_size config.write_shape = Coordinate(output_shape) * config.output_voxel_size config.inference_input_shape = config.read_shape config.inference_output_shape = config.write_shape config.block_shape = [*output_shape, metadata.out_channels] - - config.model = self.cellmap_model.ts_model - config.model.to(_get_device()) - config.model.eval() return config def to_dict(self): diff --git a/cellmap_flow/portable/__init__.py b/cellmap_flow/portable/__init__.py index 84876d35..f92d4adb 100644 --- a/cellmap_flow/portable/__init__.py +++ b/cellmap_flow/portable/__init__.py @@ -4,8 +4,21 @@ Pillow alone, so that the server runs it in its process and the browser runs the same files in chunkmirage's Pyodide workers (web/build/portable.ts bundles them as text, ``Inference.python`` calls them; ``calls.py`` is what the -browser calls). ``tests/portable/test_boundary.py`` holds every module here to -that boundary. Nothing here reads a file, a zarr or the network: the callers -do, around it (``finetune/session/volume.py``'s ``carry_paint``, +browser calls, and ``volume_calls.py`` for a volume's plan and its good +regions, ``volume.py`` and ``regions.py``). ``label_it.py`` is Label it's +segmentation of a prediction, read by +``dashboard/routes/finetune/view_labels.py`` and, through +``label_it_calls.py``, by ``web/src/backend/routes/view-labels.ts``. +``tests/portable/test_boundary.py`` holds every module here to that +boundary. Nothing here reads a file, a zarr or the network: the callers do, +around it (``finetune/session/volume.py``'s ``carry_paint`` and +``plan_volume``, ``dashboard/routes/finetune/rank_routes.py``, ``web/src/backend/routes/rank.ts``). +A finetune's training targets are ``targets.py``, which the server's +``finetune/target_transforms.py`` wraps and the browser calls through +``targets_calls.py``. +AI-assisted annotation's plane, image, mask and labels are ``ai_annotate.py`` +(the browser calls ``ai_annotate_calls.py``), and the label decisions under +it and the Finetune tab's box tools are ``fill.py`` (the browser's box tools +call ``fill_calls.py``). """ diff --git a/cellmap_flow/portable/ai_annotate.py b/cellmap_flow/portable/ai_annotate.py new file mode 100644 index 00000000..b82a5c79 --- /dev/null +++ b/cellmap_flow/portable/ai_annotate.py @@ -0,0 +1,621 @@ +"""AI-assisted annotation's maths, which both modes run: which plane of the +data an annotation reads and which voxels it writes, the image the model is +sent, the mask in the image it sends back, and the labels that mask becomes. + +The server runs it in its process (``ai_annotate/geometry.py``, +``pipeline.py``, ``mask_extraction.py``, ``staging.py`` and the Vertex +backend call it, around their zarr reads and PNG files), the browser in +chunkmirage's Pyodide worker (``portable/ai_annotate_calls.py``, around the +engine's raw reads and the paint store). Nothing here reads data, calls a +model or sees a credential: what is sent and to whom stays with each mode's +provider. + +The plane. A click (or the view centre) picks a point in world nm and the +plane the user is looking at picks the depth axis. ``plan_plane`` turns +those into a ``PlanePlan``: a square field of view around the point, +snapped to the annotation volume's voxels and clipped to the volume (the +write box, one annotation voxel thick along the depth axis), and the raw +read that covers the same physical area at the model's input voxel size (one +input voxel thick, at the point's depth). So whatever the model paints on +the image lands on the annotation voxels under it. Annotation voxels are +placed as ``finetune.session.volume`` places them: the volume's +``dataset_offset_nm`` is voxel 0's *centre*, and a voxel is in a box when +its centre is (``fill.box_voxels``). The raw is read on its own grid, from +the voxel that holds the first pixel's centre (``raw_box``). + +Axes are z, y, x throughout, as everywhere in cellmap-flow, except in the +image sent: that is laid out as the viewer's panel shows the plane +(``to_screen``), so the model, and the user reviewing its answer, see what +was on screen. Which axis runs down it and which across, and whether across +runs backwards, depend on the viewer's display dimensions (``screen_axes``): +cellmap-flow's viewers show x, y, z, so the XY plane is drawn as in z, y, x +order, the XZ plane too, and the YZ plane transposed and mirrored. + +The images are resampled by Pillow (``resize``, ``nearest``, and the +model's reply in ``model_mask``), in both modes: the server's installed +Pillow, and in the browser the one Pyodide ships, which chunkmirage's worker +loads the first time a call imports it (scipy, for the mask's objects, +likewise). +""" + +from dataclasses import asdict, dataclass + +import numpy as np + +from cellmap_flow.portable.fill import box_voxels, counts_up, seed_labels, volume_corner_nm + +AXIS_NAMES = ("z", "y", "x") + +# The longest side of an image sent to a model. A hosted image model bills +# an image by the image, not by its pixels (gemini-3-pro-image: ~560 tokens +# whatever the size), and resizes what it gets to its own working size, so +# more pixels than this cost upload time and gain nothing. +MAX_IMAGE_SIDE = 1024 + +# The plane each depth axis leaves, named by the data axes it shows. +PLANE_NAMES = ("XY", "XZ", "YZ") + +# How strongly the review's overlay tints the mask with the target colour. +OVERLAY_ALPHA = 0.5 + +# How neuroglancer's panels draw their plane, by the display-dimension slot +# each looks along: the slot drawn down, the slot drawn across, and whether +# across runs backwards. The "xy" panel (along slot 2) draws display +# dimension 0 across and 1 down; "xz" (along 1) turns it a quarter about the +# first, so 0 across and 2 down; "yz" (along 0) a quarter about the second, +# so 1 down and 2 across, increasing to the left (AXES_RELATIVE_ORIENTATION's +# quaternions in neuroglancer's data_panel_layout.ts). The image sent was z, +# y, x order before, so planes went to the model, and back to the user's +# review, transposed or mirrored from what was on screen. +_PANEL_SLOTS = {2: (1, 0, False), 1: (2, 0, False), 0: (1, 2, True)} + +# How close (relative, and at least absolutely) a value must be to a whole +# number to be that number (io.metadata.snap_integral's tolerance). +_INTEGRAL_TOLERANCE = 1e-6 + + +class Refused(Exception): + """A plane the point cannot have, outside the volume or the data: the + message says so, for the user (the server raises it as an + ``AIAnnotateError("refused")``, the browser's calls answer it).""" + + +def snap_integral(values): + """``values`` as floats, with near-whole numbers made exactly whole + (``io.metadata.snap_integral``): voxel arithmetic in nm leaves + 511.99999999 where 512 is meant, and a floor or a round after it must + not see the difference.""" + arr = np.asarray(values, dtype=float) + rounded = np.round(arr) + close = np.abs(arr - rounded) <= _INTEGRAL_TOLERANCE * np.maximum(1.0, np.abs(arr)) + return np.where(close, rounded, arr) + + +# --------------------------------------------------------------------------- +# The screen layout +# --------------------------------------------------------------------------- + + +def display_names(display_dimensions=None) -> tuple: + """``display_dimensions`` when they are z, y and x in some order, else + z, y, x: the display dimensions of a viewer that sets none over + cellmap-flow's coordinate space, and of a plan saved before plans kept + them.""" + names = tuple(display_dimensions or ()) + return names if sorted(names) == sorted(AXIS_NAMES) else AXIS_NAMES + + +def screen_axes(depth_axis, display_dimensions=None) -> tuple: + """``(down, across, backwards)``: the data axes (0 z, 1 y, 2 x) the panel + looking along ``depth_axis`` draws down and across, and whether across + runs backwards, in a viewer whose display dimensions are + ``display_dimensions``, as ``display_names`` gives them (a plan's), or + None for z, y, x.""" + names = tuple(display_dimensions or AXIS_NAMES) + down, across, backwards = _PANEL_SLOTS[names.index(AXIS_NAMES[int(depth_axis)])] + return AXIS_NAMES.index(names[down]), AXIS_NAMES.index(names[across]), backwards + + +def _screen(depth_axis, display_dimensions=None): + down, across, backwards = screen_axes(depth_axis, display_dimensions) + return down > across, backwards + + +def screen_shape(shape, depth_axis, display_dimensions=None) -> tuple: + """A plane's ``(rows, cols)`` in z, y, x order, as the panel draws it + (``screen_axes``).""" + transposed, _ = _screen(depth_axis, display_dimensions) + rows, cols = (int(s) for s in shape) + return (cols, rows) if transposed else (rows, cols) + + +def to_screen(plane, depth_axis, display_dimensions=None) -> np.ndarray: + """``plane`` (rows and columns its two axes in z, y, x order) as the panel + looking along ``depth_axis`` draws it, in a viewer showing + ``display_dimensions`` (``screen_axes``).""" + transposed, backwards = _screen(depth_axis, display_dimensions) + plane = np.asarray(plane) + if transposed: + plane = np.swapaxes(plane, 0, 1) + return plane[:, ::-1] if backwards else plane + + +def from_screen(image, depth_axis, display_dimensions=None) -> np.ndarray: + """``to_screen`` undone: an image as the panel draws it, in z, y, x order.""" + transposed, backwards = _screen(depth_axis, display_dimensions) + image = np.asarray(image) + if backwards: + image = image[:, ::-1] + return np.swapaxes(image, 0, 1) if transposed else image + + +# --------------------------------------------------------------------------- +# The plan +# --------------------------------------------------------------------------- + + +def _floats(values): + return tuple(float(v) for v in values) + + +def _ints(values): + return tuple(int(v) for v in values) + + +@dataclass(frozen=True) +class PlanePlan: + """One plane to annotate: what is read, what is sent and what is written. + + All three-element tuples are z, y, x. ``raw_offset_nm`` and + ``raw_shape_nm`` are the world box the raw is read over (lower corner + and size): in plane, the write box's extent; along the depth axis, one + input voxel centred on the point. ``image_shape`` is the (rows, cols) + of the image sent, laid out as on screen (``to_screen``), + ``resolution_nm`` its pixel size in nm (square + pixels, which a model needs to see shapes as they are). ``write_lo`` and + ``write_hi`` are the annotation voxels written, ``[lo, hi)``, clipped to + the volume and one voxel thick along the depth axis. + ``display_dimensions`` are the viewer's (``display_names``), which say + how its panel draws the plane (``screen_axes``); z, y, x in a plan saved + before plans kept them. + """ + + depth_axis: int + point_nm: tuple + raw_offset_nm: tuple + raw_shape_nm: tuple + image_shape: tuple + input_voxel_size: tuple + write_lo: tuple + write_hi: tuple + resolution_nm: float + display_dimensions: tuple = AXIS_NAMES + + @property + def plane_axes(self) -> tuple: + """The two in-plane axes, in z, y, x order.""" + return tuple(a for a in range(3) if a != self.depth_axis) + + @property + def plane_name(self) -> str: + """"XY", "XZ" or "YZ": the data axes the plane shows.""" + return PLANE_NAMES[self.depth_axis] + + @property + def write_shape(self) -> tuple: + """The write box's in-plane shape in annotation voxels, its ``plane_axes`` in z, y, x order.""" + return tuple(int(self.write_hi[a] - self.write_lo[a]) for a in self.plane_axes) + + @property + def click_px(self) -> tuple: + """``(row, col)`` of the point in the image sent.""" + down, across, backwards = screen_axes(self.depth_axis, self.display_dimensions) + position = [] + for side, a in zip(self.image_shape, (down, across)): + fraction = (self.point_nm[a] - self.raw_offset_nm[a]) / self.raw_shape_nm[a] + position.append(int(np.clip(np.floor(fraction * side), 0, side - 1))) + if backwards: + # The pixel counted from the near end, then mirrored, so a point + # on a boundary takes the pixel it takes in z, y, x order. + position[1] = self.image_shape[1] - 1 - position[1] + return tuple(position) + + def to_screen(self, plane) -> np.ndarray: + """``to_screen`` for this plan's plane.""" + return to_screen(plane, self.depth_axis, self.display_dimensions) + + def from_screen(self, image) -> np.ndarray: + """``from_screen`` for this plan's plane.""" + return from_screen(image, self.depth_axis, self.display_dimensions) + + def to_dict(self) -> dict: + """The plan as JSON-safe lists, for a staging's meta.json and the browser.""" + return {key: list(value) if isinstance(value, tuple) else value for key, value in asdict(self).items()} + + @classmethod + def from_dict(cls, data: dict) -> "PlanePlan": + """The plan ``to_dict`` wrote.""" + return cls( + depth_axis=int(data["depth_axis"]), + point_nm=_floats(data["point_nm"]), + raw_offset_nm=_floats(data["raw_offset_nm"]), + raw_shape_nm=_floats(data["raw_shape_nm"]), + image_shape=_ints(data["image_shape"]), + input_voxel_size=_floats(data["input_voxel_size"]), + write_lo=_ints(data["write_lo"]), + write_hi=_ints(data["write_hi"]), + resolution_nm=float(data["resolution_nm"]), + display_dimensions=display_names(data.get("display_dimensions")), + ) + + +def plan_plane(volume: dict, volume_shape, point_nm, depth_axis: int, crop_size_px: int, + data_box_nm=None, display_dimensions=None) -> PlanePlan: + """The plane through ``point_nm`` (world nm, z, y, x) across ``depth_axis``. + + ``volume`` is the annotation volume's record (``output_voxel_size``, + ``input_voxel_size``, ``dataset_offset_nm``), ``volume_shape`` its + ``annotation/s0`` shape. The field of view is square in nm, + ``crop_size_px`` annotation voxels along the finer in-plane axis (both, + for isotropic voxels), centred on the point; its write box is the + annotation voxels whose centres lie in it, clipped to the volume, in the + annotation voxel that holds the point along the depth axis. + + ``data_box_nm`` is the raw data's extent, ``(lo, hi)`` world nm z, y, x: + the field is clipped to it too. The volume is the data rounded up to + whole chunks of the model's output, which for Cellpose at 64 nm is up to + 33 um past the data's edge, and there the plane is only the read's + zero padding: an image of it was labelled as background, and the cells + beside it smeared into it. + + The raw is read over the clipped box's extent, so the image and the box + cover the same physical area. The image's pixels are the finer in-plane + input voxel size, square; when that makes a side longer than + ``MAX_IMAGE_SIDE`` the pixels are made larger to fit. The image is laid + out as the viewer's panel draws the plane, for the viewer's + ``display_dimensions`` (z, y, x when None). + + Raises ``Refused`` when the point is outside the volume or the data: a + field clipped to an edge it does not reach is not what was aimed at. + ValueError for a depth axis, crop size or point that is not one. + """ + if depth_axis not in (0, 1, 2): + raise ValueError(f"depth_axis must be 0, 1 or 2 (z, y, x), got {depth_axis!r}") + if int(crop_size_px) < 1: + raise ValueError(f"crop_size_px must be at least 1, got {crop_size_px!r}") + output_voxel_size = np.asarray(volume["output_voxel_size"], dtype=float) + input_voxel_size = np.asarray(volume["input_voxel_size"], dtype=float) + shape = np.asarray(volume_shape, dtype=int) + corner = volume_corner_nm(volume.get("dataset_offset_nm"), output_voxel_size) + point = np.asarray(point_nm, dtype=float) + if point.shape != (3,) or not np.all(np.isfinite(point)): + raise ValueError(f"point_nm must be three finite numbers (z, y, x), got {point_nm!r}") + + voxel = np.floor(snap_integral((point - corner) / output_voxel_size)).astype(int) + if np.any(voxel < 0) or np.any(voxel >= shape): + extent = [f"{c:g}..{c + n * v:g}" for c, n, v in zip(corner, shape, output_voxel_size)] + raise Refused( + f"The point ({', '.join(f'{p:g}' for p in point)}) nm is outside the annotation volume " + f"(z, y, x {', '.join(extent)} nm): move the view onto the data and try again." + ) + + plane = [a for a in range(3) if a != depth_axis] + side_nm = int(crop_size_px) * float(output_voxel_size[plane].min()) + offset = point - side_nm / 2 + size = np.full(3, side_nm) + if data_box_nm is not None: + data_lo, data_hi = (np.asarray(v, dtype=float) for v in data_box_nm) + if np.any(point < data_lo) or np.any(point >= data_hi): + extent = [f"{lo:g}..{hi:g}" for lo, hi in zip(data_lo, data_hi)] + raise Refused( + f"The point ({', '.join(f'{p:g}' for p in point)}) nm is outside the data " + f"(z, y, x {', '.join(extent)} nm): move the view onto the data and try again." + ) + for a in plane: + top = min(offset[a] + size[a], data_hi[a]) + offset[a] = max(offset[a], data_lo[a]) + size[a] = top - offset[a] + # Along the depth axis, exactly the voxel that holds the point: a box + # one voxel long whose only centre is that voxel's. + offset[depth_axis] = corner[depth_axis] + voxel[depth_axis] * output_voxel_size[depth_axis] + size[depth_axis] = output_voxel_size[depth_axis] + box = box_voxels(volume, offset, size, shape) + if box is None: # cannot happen with the point inside, but never write an empty box + raise Refused("No annotation voxels lie around the point.") + lo, hi = box + + raw_offset = corner + lo * output_voxel_size + raw_shape = (hi - lo) * output_voxel_size + raw_offset[depth_axis] = point[depth_axis] - input_voxel_size[depth_axis] / 2 + raw_shape[depth_axis] = input_voxel_size[depth_axis] + + extent = raw_shape[plane] + resolution = max(float(input_voxel_size[plane].min()), float(extent.max()) / MAX_IMAGE_SIDE) + display = display_names(display_dimensions) + image_shape = screen_shape(np.maximum(np.round(snap_integral(extent / resolution)), 1), depth_axis, display) + return PlanePlan( + depth_axis=int(depth_axis), + point_nm=_floats(point), + raw_offset_nm=_floats(snap_integral(raw_offset)), + raw_shape_nm=_floats(snap_integral(raw_shape)), + image_shape=_ints(image_shape), + input_voxel_size=_floats(input_voxel_size), + write_lo=_ints(lo), + write_hi=_ints(hi), + resolution_nm=float(snap_integral(resolution)), + display_dimensions=display, + ) + + +def raw_box(plan: PlanePlan, voxel_size, corner) -> tuple: + """``(begin, count)``: the raw voxels to read for ``plan``, on a raw grid + of ``voxel_size`` whose voxel 0's lower corner is ``corner`` (nm, z, y, x). + + Each axis starts at the voxel that holds the first sample's centre (half + a voxel in from the box's corner) and runs for as many voxels as fit in + the box, at least one: so a box on the grid reads exactly its voxels, + and the depth axis reads the voxel that holds the point. + """ + voxel_size = np.asarray(voxel_size, dtype=float) + corner = np.asarray(corner, dtype=float) + first_centre = np.asarray(plan.raw_offset_nm, dtype=float) + voxel_size / 2 + begin = np.floor(snap_integral((first_centre - corner) / voxel_size)).astype(int) + count = np.maximum(np.round(snap_integral(np.asarray(plan.raw_shape_nm) / voxel_size)), 1).astype(int) + return _ints(begin), _ints(count) + + +# --------------------------------------------------------------------------- +# Resampling, by Pillow +# --------------------------------------------------------------------------- +# +# Pillow is imported inside the functions that resample, not at the top: in +# the browser, chunkmirage's Pyodide worker loads a package the first time a +# call fails to import it (found by its module in Pyodide's lockfile), so +# Pillow is downloaded with the first image resized, not when a dataset +# opens, and never on a page that does not annotate with AI. Each answer is +# a writable array of its own, not a view of the image's bytes. + + +def resize(plane, shape) -> np.ndarray: + """``plane`` resampled to ``shape`` (rows, cols), float32; itself when it already is that. + + Shrinking (neither side grows) averages each pixel's footprint (Pillow's + box filter), so fine membranes fade rather than alias; otherwise (the + coarse axis of an anisotropic plane) it interpolates linearly (Pillow's + bilinear), on Pillow's 32-bit float ("F") image. + """ + rows, cols = (int(s) for s in shape) + plane = np.asarray(plane) + if plane.shape == (rows, cols): + return plane + if rows < 1 or cols < 1: + raise ValueError(f"cannot resize a plane to {rows}x{cols}") + from PIL import Image + + shrinking = plane.shape[0] >= rows and plane.shape[1] >= cols + kind = Image.Resampling.BOX if shrinking else Image.Resampling.BILINEAR + image = Image.fromarray(np.ascontiguousarray(plane, dtype=np.float32)) + return np.array(image.resize((cols, rows), kind), dtype=np.float32) + + +def nearest(array, shape) -> np.ndarray: + """A 2D ``array`` (uint8 or int32, as Pillow holds an image of each) at + ``shape`` (rows, cols) by nearest neighbour, pixel centre to pixel centre + (Pillow's NEAREST), in its own dtype; a copy when it already is that + size.""" + rows, cols = (int(s) for s in shape) + array = np.asarray(array) + if array.shape == (rows, cols): + return array.copy() + from PIL import Image + + image = Image.fromarray(np.ascontiguousarray(array)) + return np.array(image.resize((cols, rows), Image.Resampling.NEAREST), dtype=array.dtype) + + +# --------------------------------------------------------------------------- +# The image sent, and the mask in the image the model sends back +# --------------------------------------------------------------------------- + + +def plane_image(block, plan: PlanePlan) -> np.ndarray: + """The plane sent, from the raw ``block`` read over ``raw_box``'s voxels (z, y, x). + + Its one slice along the depth axis, laid out as on screen + (``PlanePlan.to_screen``) and resampled to the plan's image + (``resize``). The raw's dtype is kept unless the plane had to be resized + to the image's grid, when it is float32. + """ + plane = np.take(np.asarray(block), 0, axis=plan.depth_axis) + return resize(plan.to_screen(plane), plan.image_shape) + + +def normalize_to_uint8(data: np.ndarray) -> np.ndarray: + """A 2D array as uint8 grey levels (from ask-to-mask's ``_normalize_to_uint8``). + + uint8 passes through unchanged. Anything else is clipped to its 1st-99th + percentile over the plane and stretched to 0-255, as raw EM is not always + 8-bit and a few saturated pixels should not flatten the contrast. + """ + data = np.asarray(data) + if data.dtype == np.uint8: + return data + p_lo, p_hi = np.percentile(data, (1, 99)) + if p_hi <= p_lo: + p_hi = p_lo + 1 + clipped = np.clip(data.astype(np.float32), p_lo, p_hi) + return ((clipped - p_lo) / (p_hi - p_lo) * 255).astype(np.uint8) + + +def extract_mask(rgb, target_rgb, threshold: float = 200.0) -> np.ndarray: + """The pixels of the model's reply (rows, cols, 3 or 4, uint8) painted + ``target_rgb``, as a bool (rows, cols) mask (ask-to-mask's + ``extract_mask``, its colour-difference mode). + + Each pixel scores how strongly it shows the target colour: its weakest + channel among those the target has at full strength, minus its strongest + deviation elsewhere -- a channel the target has off (0) being on, or a + channel the target has at part strength being away from that strength. + For red (255, 0, 0) that is ``R - max(G, B)``: red that dominates. A + pixel counts when its score reaches ``threshold``. + + The part-strength term is what lets orange (255, 128, 0) or purple + (128, 0, 255) be found at all: counting their half channel as "on" would + cap the score at 128, under the threshold, and counting it as "off" + would reject the colour itself. + """ + out = np.asarray(rgb)[:, :, :3].astype(np.float32) + target = np.asarray(target_rgb, dtype=np.float32) + + full = target >= 255 + off = target <= 0 + part = ~full & ~off + + strength = np.min(out[:, :, full], axis=-1) if full.any() else np.full(out.shape[:2], 255.0, np.float32) + deviations = [] + if off.any(): + deviations.append(np.max(out[:, :, off], axis=-1)) + if part.any(): + deviations.append(np.max(np.abs(out[:, :, part] - target[part]), axis=-1)) + score = strength - np.max(deviations, axis=0) if deviations else strength + return score >= threshold + + +def model_mask(rgb, sent_shape, image_shape, target_rgb): + """The mask in a model's reply, and the reply lined up with the plane: + ``(mask, aligned)``. + + ``rgb`` is the image the model sent back, decoded (rows, cols, 3 or 4, + uint8), ``sent_shape`` the (rows, cols) of the image it was sent (the + plane padded to a square), ``image_shape`` the plane's. A reply at + another size (or shape: stretching it then is what the review shows) is + resized back to the sent size by Pillow's bilinear filter on an RGB + image (which averages when shrinking), then the padding is cut off the + right and bottom, so what is left lines up with the plane; the mask is + the pixels painted ``target_rgb`` in that (``extract_mask``). + ``aligned`` is (rows, cols, 3) uint8, the plane's shape, for the review. + """ + rgb = np.asarray(rgb, dtype=np.uint8)[:, :, :3] + sent_rows, sent_cols = (int(s) for s in sent_shape) + if rgb.shape[:2] != (sent_rows, sent_cols): + from PIL import Image + + reply = Image.fromarray(np.ascontiguousarray(rgb)) + rgb = np.array(reply.resize((sent_cols, sent_rows), Image.Resampling.BILINEAR)) + rows, cols = (int(s) for s in image_shape) + if rows > rgb.shape[0] or cols > rgb.shape[1]: + raise ValueError(f"The plane is {rows}x{cols}, larger than the {rgb.shape[0]}x{rgb.shape[1]} image sent") + aligned = np.ascontiguousarray(rgb[:rows, :cols]) + return extract_mask(aligned, target_rgb), aligned + + +def overlay(rgb, mask, target_rgb, alpha: float = OVERLAY_ALPHA) -> np.ndarray: + """``rgb`` (the image sent, rows, cols, 3 or 4) with the pixels ``mask`` + marks tinted ``target_rgb``: the review's third picture, what accepting + writes, not the model's own image. ``mask`` is the write box's objects + laid out as the image is (``to_screen``) and scaled to it by nearest + neighbour; uint8 RGB back, rounded half to even.""" + out = np.asarray(rgb)[:, :, :3].astype(np.float32) + on = nearest((np.asarray(mask) > 0).astype(np.uint8), out.shape[:2]) > 0 + tint = np.asarray(target_rgb, dtype=np.float32) + out[on] = (1 - alpha) * out[on] + alpha * tint + return np.round(out).astype(np.uint8) + + +# --------------------------------------------------------------------------- +# The mask on the write box, and its labels +# --------------------------------------------------------------------------- + + +def mask_to_write_shape(mask: np.ndarray, plan: PlanePlan) -> np.ndarray: + """The objects of ``mask`` (the image's shape, as on screen) on the write + box's in-plane voxels, in z, y, x order (``PlanePlan.write_shape``): + int32, 0 for background and 1..n for the objects, numbered in the box's + raster order as ``scipy.ndimage.label`` numbers a mask. + + Objects are the mask's 4-connected pieces (scipy's default, so pieces + touching only at a corner stay apart, as the model was asked to leave + them), found at the image's own resolution, where the model drew the + lines between them: on voxels coarser than the pixels a line can fall + between the pixels the voxels take, and the objects either side of it + still keep ids of their own. Then nearest neighbour, pixel centre to + voxel centre: each annotation voxel takes the object of the pixel at + its centre, so the mask keeps hard edges and lands where it was drawn + whatever the two sizes are. An object no voxel centre falls in is lost. + """ + from scipy.ndimage import label + + mask = np.asarray(mask, dtype=bool) + if mask.ndim != 2: + raise ValueError(f"The mask must be 2D, got shape {mask.shape}") + objects, _ = label(mask) + rows, cols = screen_shape(plan.write_shape, plan.depth_axis, plan.display_dimensions) + objects = nearest(objects.astype(np.int32), (rows, cols)) + objects = plan.from_screen(objects) + # Renumbered by first voxel, so fresh ids go to the objects in the order + # labelling the box itself would give them, whatever the screen layout. + ids, first = np.unique(objects, return_index=True) + order = ids[ids > 0][np.argsort(first[ids > 0])] + number = np.zeros(int(objects.max(initial=0)) + 1, dtype=np.int32) + number[order] = np.arange(1, order.size + 1) + return number[objects] + + +def labels_for_box(objects: np.ndarray, existing: np.ndarray, depth_axis: int, count_up: bool, + beside=None) -> np.ndarray: + """The labels for the write box: an id per object of ``objects``, 1 elsewhere. + + ``objects`` is the in-plane objects (``mask_to_write_shape``), 0 for + background, ``existing`` the one-voxel-thick box as the volume holds + it. The objects are labelled by ``fill.seed_labels``: an object the user + has already painted part of keeps that id, the rest get fresh ones, + counting up for an instance target's ``count_up``. + + ``beside`` is the planes on either side of the box along the depth axis, + as the volume holds them (each shaped like ``existing``, or None at the + volume's edge). Planes are annotated one at a time, so an object usually + continues one in the plane next to it: where this plane is unpainted, + the ids beside it count as painted here, and an object takes the id it + overlaps most, rather than a fresh one that an instance target would + read as a boundary between the two planes. Fresh ids skip those planes' + ids too, so a new object does not take the id of a different object + right next to it, which an instance target would read as one object. + """ + objects = np.expand_dims(np.asarray(objects), depth_axis) + if objects.shape != existing.shape: + raise ValueError(f"The mask makes a box of {objects.shape}; the box holds {existing.shape}") + planes = [np.asarray(p, dtype=existing.dtype) for p in (beside or ()) if p is not None] + if any(p.shape != existing.shape for p in planes): + raise ValueError(f"The planes beside the box must be shaped {existing.shape}") + if not planes: + return seed_labels(objects, existing, count_up=count_up) + seen = existing.copy() + for plane in planes: + carry = (seen == 0) & (plane >= 2) + seen[carry] = plane[carry] + # The planes beside are stacked after the box with no objects in them: + # seed_labels labels only the box's objects, but its fresh ids see every + # id in the stack. + stack_existing = np.concatenate([seen, *planes], axis=depth_axis) + stack_objects = np.concatenate([objects, *[np.zeros_like(objects)] * len(planes)], axis=depth_axis) + labels = seed_labels(stack_objects, stack_existing, count_up=count_up) + return np.take(labels, [0], axis=depth_axis) + + +def accept_labels(objects, existing, depth_axis: int, instance_target: bool, overwrite: bool, + beside=None) -> np.ndarray: + """What Accept writes into the write box: ``labels_for_box``'s labels, as + a seed's ids (counting up for an instance target on a volume of more + than 8-bit ids, ``fill.counts_up``; an object continuing one in a plane + ``beside`` keeps its id), for ``fill.paint`` to write. + + With ``overwrite`` the model's objects replace what is under them, but + its background only fills unlabelled voxels: an object already painted + that the model missed is kept, not erased to background (those voxels + get 0, "leave as it is"). + """ + labels = labels_for_box(objects, existing, depth_axis, count_up=counts_up(instance_target, existing.dtype), + beside=beside) + if overwrite: + labels[(labels == 1) & (existing != 0)] = 0 + return labels diff --git a/cellmap_flow/portable/ai_annotate_calls.py b/cellmap_flow/portable/ai_annotate_calls.py new file mode 100644 index 00000000..85a953ed --- /dev/null +++ b/cellmap_flow/portable/ai_annotate_calls.py @@ -0,0 +1,136 @@ +"""What the browser calls for an AI annotation: the functions +``web/src/backend/ai-annotate-job.ts`` and ``web/src/ai-annotate/gemini.ts`` +run through chunkmirage's ``Engine.call("cellmap_flow.portable.ai_annotate_calls:", +args, arrays)`` (``chunkmirage.calls`` says the shape: ``fn(args, arrays)``, +``args`` the JSON arguments, ``arrays`` the bytes sent beside them, in order; +a JSON value, bytes, or a pair of JSON and arrays back). Each decodes its +arrays, calls the maths in ``ai_annotate.py`` and ``fill.py`` and encodes the +answer; the maths is not here. Images travel as the canvas holds them, RGBA +bytes rows first; label boxes in the volume's dtype, C order (z, y, x). + +A refusal meant for the user (a point outside the volume or the data, ids +running out) is answered as ``{"refused": message}`` rather than raised: the +worker would hand the page Python's traceback with it, where the page shows +the message alone, as the server's routes do. Nothing here logs or keeps +anything: an image's bytes go no further than the answer. +""" + +import numpy as np + +from cellmap_flow.portable import ai_annotate, fill +from cellmap_flow.portable.ai_annotate import PlanePlan, Refused + + +def _rgba(rgb) -> np.ndarray: + """An RGB image (rows, cols, 3) as the canvas's RGBA, opaque.""" + out = np.full(rgb.shape[:2] + (4,), 255, dtype=np.uint8) + out[:, :, :3] = rgb + return out + + +def plan(args, arrays): + """The plane through the point (``ai_annotate.plan_plane``) and the raw it + reads (``raw_box``). ``args``: ``volume`` (its ``output_voxel_size``, + ``input_voxel_size`` and ``dataset_offset_nm``), ``volume_shape``, + ``point_nm``, ``depth_axis``, ``crop_size_px``, ``data_box_nm`` and + ``display_dimensions`` (each null for none), and ``raw``, the grid of the + raw level read (``voxel``, and ``corner``, its voxel 0's lower corner in + nm). Answers the ``plan`` (``PlanePlan.to_dict``), its ``click_px`` and + ``write_shape``, and the ``raw_box`` (``begin``, ``count``); or the + refusal.""" + try: + planned = ai_annotate.plan_plane( + args["volume"], args["volume_shape"], args["point_nm"], int(args["depth_axis"]), int(args["crop_size_px"]), + data_box_nm=args.get("data_box_nm"), display_dimensions=args.get("display_dimensions"), + ) + except Refused as e: + return {"refused": str(e)} + begin, count = ai_annotate.raw_box(planned, args["raw"]["voxel"], args["raw"]["corner"]) + return { + "plan": planned.to_dict(), + "click_px": list(planned.click_px), + "write_shape": list(planned.write_shape), + "raw_box": {"begin": list(begin), "count": list(count)}, + } + + +def request_image(args, arrays): + """The image sent for ``plan``'s plane: the raw block its ``raw_box`` + read (``arrays[0]``, ``count`` voxels z, y, x of ``dtype``, 0 past the + data) laid out and resampled (``ai_annotate.plane_image``) and scaled to + 8-bit grey (``normalize_to_uint8``), as the server's read_plane and + build_request make it. Answers its RGBA bytes, the plan's + ``image_shape``, the grey in every channel.""" + planned = PlanePlan.from_dict(args["plan"]) + block = np.frombuffer(arrays[0], dtype=np.dtype(args["dtype"])).reshape(tuple(args["count"])) + grey = ai_annotate.normalize_to_uint8(ai_annotate.plane_image(block, planned)) + return _rgba(np.repeat(grey[:, :, None], 3, axis=2)) + + +def model_mask(args, arrays): + """The mask in a model's reply, and the reply lined up with the plane + (``ai_annotate.model_mask``). ``args``: ``shape`` (rows, cols) of the + reply as decoded, ``sent`` and ``image`` (rows, cols of the image sent, + padded square, and of the plane), ``target_rgb``; ``arrays``: the + reply's RGBA bytes. Answers ``{}`` with the mask (a byte a pixel of the + plane, 1 where painted) and the lined-up reply (RGBA).""" + rows, cols = (int(s) for s in args["shape"]) + reply = np.frombuffer(arrays[0], dtype=np.uint8).reshape(rows, cols, 4) + mask, aligned = ai_annotate.model_mask(reply, args["sent"], args["image"], args["target_rgb"]) + return {}, [mask.astype(np.uint8), _rgba(aligned)] + + +def prepare(args, arrays): + """Nothing, once scipy.ndimage is imported, which ``stage`` labels the + mask's objects with: the browser calls this while the model works, so + scipy's first load (seconds, once a page) is over by the time the + answer comes.""" + import scipy.ndimage # noqa: F401 + + return None + + +def stage(args, arrays): + """What the review shows of a mask, as the server stages it: its objects + on the write box (``ai_annotate.mask_to_write_shape``), the share of the + box they mark, and the image sent with them tinted (``overlay``). + ``args``: ``plan`` and ``target_rgb``; ``arrays``: the mask (a byte a + pixel of the plan's image, nonzero where painted) and the image sent + (RGBA). Answers ``mask_fraction`` with the objects (int32, the write + box's in-plane shape, z, y, x order) and the overlay (RGBA).""" + planned = PlanePlan.from_dict(args["plan"]) + rows, cols = planned.image_shape + mask = np.frombuffer(arrays[0], dtype=np.uint8).reshape(rows, cols) + image = np.frombuffer(arrays[1], dtype=np.uint8).reshape(rows, cols, 4) + objects = ai_annotate.mask_to_write_shape(mask, planned) + tinted = ai_annotate.overlay(image, planned.to_screen(objects > 0), args["target_rgb"]) + fraction = float((objects > 0).mean()) if objects.size else 0.0 + return {"mask_fraction": fraction}, [objects.astype(np.int32), _rgba(tinted)] + + +def accept(args, arrays): + """What Accept writes into the write box: ``ai_annotate.accept_labels``, + written over the box as ``fill.paint`` writes. ``args``: ``depth_axis``, + ``shape`` (the write box's, z, y, x), ``dtype`` (the volume's), + ``instance_target`` (the model is trained on instances), ``overwrite``, + and ``beside``: whether the planes before and after the box along the + depth axis are sent (false past the volume's edge). ``arrays``: the + objects (int32, as ``stage`` answered them), the box as the volume holds + it now, then the planes beside that are sent, each shaped as the box. + Answers the counts (``n_foreground``, ``n_background``, + ``n_overwritten``) with the box to write, or no array when nothing + changes; or the refusal, when the ids run out.""" + shape, dtype, depth_axis = tuple(args["shape"]), np.dtype(args["dtype"]), int(args["depth_axis"]) + in_plane = tuple(n for a, n in enumerate(shape) if a != depth_axis) + objects = np.frombuffer(arrays[0], dtype=np.int32).reshape(in_plane) + existing = np.frombuffer(arrays[1], dtype=dtype).reshape(shape) + sent = iter(arrays[2:]) + beside = [np.frombuffer(next(sent), dtype=dtype).reshape(shape) if present else None for present in args["beside"]] + overwrite = bool(args["overwrite"]) + try: + labels = ai_annotate.accept_labels(objects, existing, depth_axis, bool(args["instance_target"]), overwrite, beside) + except ValueError as e: + return {"refused": str(e)} + after, (n_foreground, n_background, n_overwritten) = fill.paint(existing, labels, overwrite) + counts = {"n_foreground": n_foreground, "n_background": n_background, "n_overwritten": n_overwritten} + return counts, [] if after is None else [after] diff --git a/cellmap_flow/portable/fill.py b/cellmap_flow/portable/fill.py new file mode 100644 index 00000000..aea6f6c3 --- /dev/null +++ b/cellmap_flow/portable/fill.py @@ -0,0 +1,280 @@ +"""Labelling a box of an annotation volume: the decisions behind the Finetune +tab's Seed, All Background and Split Objects and an accepted AI annotation +(``finetune/session/fill.py`` says what each is for), on arrays alone, so the +server makes them over MinIO's zarr and the browser over its paint store's +chunks (``portable/fill_calls.py`` for the Label it tools, +``portable/ai_annotate_calls.py`` for an accept). + +- ``box_voxels``: the annotation voxels a box in world nm covers. +- ``seed_labels``: the ids a box gets from a segmentation: the id the user + painted on an object kept, fresh ones for the rest (``fresh_ids``). +- ``relabel_objects``: the box's foreground relabelled by connected component. +- ``paint``, ``rewrite`` and ``restore``: which voxels a write changes and + an undo puts back, with the counts the routes answer. + +Nothing here reads or writes a volume: the callers read the box, hand it in +and write back what comes out (``finetune/session/fill.py`` through zarr +over MinIO, ``web/src/backend/routes/view-labels.ts`` and +``web/src/backend/ai-annotate-job.ts`` through the paint store's +``web/src/ai-annotate/fill.ts``). +""" + +import numpy as np + + +def volume_corner_nm(dataset_offset_nm, output_voxel_size) -> np.ndarray: + """The world position of an annotation volume's voxel-0 lower corner, in nm. + + ``dataset_offset_nm`` is voxel 0's *centre*, the OME translation, as + ``finetune.session.volume.volume_corner_nm`` reads it (its docstring says + why), so the corner is half a voxel before it; voxel 0's corner is at + minus half a voxel when the volume has no offset. + """ + voxel_size = np.asarray(output_voxel_size, dtype=float) + offset = np.zeros(3) if dataset_offset_nm is None else np.asarray(dataset_offset_nm, dtype=float) + return offset - voxel_size / 2 + + +def box_voxels(volume, offset_nm, shape_nm, volume_shape): + """``(lo, hi)``: the annotation voxels whose centres lie in a box, clipped to the volume. + + ``offset_nm`` and ``shape_nm`` are the box's lower corner and size in + world nm, as a good region records them; ``volume`` is the volume's + record, for its grid (``output_voxel_size``, ``dataset_offset_nm``). + None when no voxel of the volume is in the box. + """ + voxel_size = np.asarray(volume["output_voxel_size"], dtype=float) + corner = volume_corner_nm(volume.get("dataset_offset_nm"), voxel_size) + start = (np.asarray(offset_nm, dtype=float) - corner) / voxel_size + end = start + np.asarray(shape_nm, dtype=float) / voxel_size + # Voxel i's centre is at i + 0.5 voxels from the corner. + lo = np.maximum(np.ceil(start - 0.5), 0).astype(int) + hi = np.minimum(np.ceil(end - 0.5), np.asarray(volume_shape)).astype(int) + if np.any(hi <= lo): + return None + return lo, hi + + +def fresh_ids(existing, count, count_up): + """``count`` ids for new objects in a box that holds ``existing``. + + ``count_up``: from one past the box's largest label, so an affinity + target's objects stay apart from every object nearby (ids elsewhere in + the volume are not looked at: affinities only compare nearby voxels). + Else the lowest ids the box does not use, so a uint8 volume's 254 ids + are never run through: a repeat in another box is only a repeated + colour, which a binary or distance target does not see. Raises + ValueError when the dtype has no room. + """ + biggest = np.iinfo(existing.dtype).max + if count_up: + first = max(int(existing.max()), 1) + 1 + ids = np.arange(first, first + count) + else: + used = np.unique(existing[existing >= 2]).astype(np.int64) + candidates = np.arange(2, min(biggest, len(used) + count + 2) + 1) + ids = np.setdiff1d(candidates, used, assume_unique=True)[:count] + if ids.size < count or (ids.size and ids[-1] > biggest): + raise ValueError( + f"{count} new objects do not fit in the volume's {existing.dtype} labels: make a new annotation " + "volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min " + "size or the threshold so the seed makes fewer objects" + ) + return ids + + +def counts_up(instance_target, dtype) -> bool: + """Whether new objects' ids count up from the box's largest (``fresh_ids``): + for a model trained on instances (``instance_target``: affinities, or + Cellpose's flows) whose volume holds more than 8-bit ids. A uint8 volume + reuses the ids its box leaves free, whatever the model, or its 254 would + soon run out.""" + return bool(instance_target) and np.dtype(dtype).itemsize > 1 + + +def _pair_counts(a, b): + """The distinct pairs ``(a[i], b[i])`` of two arrays of non-negative + int64 values, as the columns of a (2, n) array in order of ``a`` and then + ``b``, and how often each occurs: ``np.unique(np.stack([a, b]), axis=1, + return_counts=True)``, by one key per pair, ``a * (max(b) + 1) + b``, + which sorts in the same order. Sorting one array of keys is many times + quicker than sorting the pairs as rows (about 1 ms against 75 for the + foreground of a 64^3 box, natively, and the browser's numpy is slower + still). The keys stay within int64 for any label dtype up to uint32 in a + box of fewer than 2^31 voxels (``a`` counts objects, at most one a voxel). + """ + base = int(b.max(initial=0)) + 1 + keys, counts = np.unique(a * base + b, return_counts=True) + return np.stack([keys // base, keys % base]), counts + + +def seed_labels(objects, existing, count_up=False): + """Labels for a box from a segmentation: an id per object inside, 1 outside. + + ``objects`` is a segmenter's output over the box (0 background, objects + 1..n), ``existing`` what the box holds now. Each object gets an id of + its own (``fresh_ids``; ``count_up`` for an instance target), so + neighbouring objects are told apart, and a merge the model made shows + as one colour to split; but an object the user has already painted part + of takes the id painted there most, or the unpainted rest of it would be + taught as a different object from its painted part. A binary or distance + target reads every id as foreground, so the ids cost it nothing. + """ + objects = np.asarray(objects).astype(np.int64, copy=False) + foreground = objects > 0 + labels = np.where(foreground, 2, 1).astype(existing.dtype) + if not foreground.any(): + return labels + n = int(objects.max()) + ids = np.zeros(n + 1, dtype=np.int64) + painted = (existing >= 2) & foreground + if painted.any(): + pairs, counts = _pair_counts(objects[painted], existing[painted].astype(np.int64)) + # Most-painted last, so its id is the one that stays. + order = np.argsort(counts, kind="stable") + ids[pairs[0, order]] = pairs[1, order] + present = np.zeros(n + 1, dtype=bool) + present[np.unique(objects[foreground])] = True + fresh = np.flatnonzero(present & (ids == 0)) + ids[fresh] = fresh_ids(existing, fresh.size, count_up) + labels[foreground] = ids[objects[foreground]].astype(existing.dtype) + return labels + + +def connected_components(mask, connectivity=1, per_slice=False): + """An id per connected object of ``mask`` (its nonzero voxels), 1..n, 0 elsewhere. + + ``scipy.ndimage.label`` with the structure of ``connectivity`` (1 face + neighbours, 2 and edges, 3 every neighbour, at most the array's + dimensions), numbered as it numbers them: by the raster order of each + object's first voxel. ``per_slice``: each z slice of a (z, y, x) mask is + labelled on its own, in 2D, its ids following the slice before's. As + ``post.segment.connected_components`` labels a seed's objects, without + its minimum size. + """ + from scipy.ndimage import generate_binary_structure, label + + mask = np.asarray(mask) != 0 + if not per_slice: + labels, _ = label(mask, generate_binary_structure(mask.ndim, min(int(connectivity), mask.ndim))) + return labels.astype(np.int64) + if mask.ndim != 3: + raise ValueError(f"per_slice labels (z, y, x) masks, got shape {mask.shape}") + structure = generate_binary_structure(2, min(int(connectivity), 2)) + labels = np.zeros(mask.shape, dtype=np.int64) + count = 0 + for z, plane in enumerate(mask): + plane_labels, n = label(plane, structure) + labels[z] = np.where(plane_labels > 0, plane_labels + count, 0) + count += n + return labels + + +def relabel_objects(existing, count_up=False, connectivity=1, per_slice=False): + """The box's foreground (labels 2 and up) relabelled by connected component. + + Each component takes the id most of its voxels hold, so a stroke that + joins two objects merges them into the id of the bigger. An id left on + several components, an object a background wall cut in two, stays on the + largest and the others get fresh ids (``fresh_ids``). Background (1) + and unpainted (0) voxels are left alone. Returns ``(labels, counts)``: + ``counts`` has ``objects``, ``split`` (components given a fresh id) and + ``merged`` (components that held more than one id). + + A cut has to go through every slice the object spans: the brush paints + one slice, and an object cut in one slice is still one object above and + below it. ``split`` is 0 then, which the page says. With ``per_slice`` + each z slice's pieces are components on their own, so a wall in one + slice splits that slice, and every slice of an object gets its own id. + ``connectivity``: as for ``connected_components``. + """ + labels = existing.copy() + foreground = existing >= 2 + counts = {"objects": 0, "split": 0, "merged": 0} + if not foreground.any(): + return labels, counts + components = connected_components(foreground, connectivity, per_slice=per_slice) + n = int(components.max()) + comp, old = components[foreground], existing[foreground].astype(np.int64) + pairs, pair_counts = _pair_counts(comp, old) + order = np.argsort(pair_counts, kind="stable") + majority = np.zeros(n + 1, dtype=np.int64) + majority[pairs[0, order]] = pairs[1, order] # the most voxels last, so it stays + counts["objects"] = int(n) + counts["merged"] = int(np.count_nonzero(np.bincount(pairs[0], minlength=n + 1) > 1)) + + sizes = np.bincount(comp, minlength=n + 1) + fresh = [] + for old_id in np.unique(majority[1:]): + claimants = np.flatnonzero(majority == old_id) + if claimants.size > 1: + keep = claimants[np.argmax(sizes[claimants])] + fresh.extend(int(c) for c in claimants if c != keep) + new = majority.copy() + if fresh: + new[fresh] = fresh_ids(existing, len(fresh), count_up) + counts["split"] = len(fresh) + labels[foreground] = new[comp].astype(existing.dtype) + return labels, counts + + +def paint(existing, labels, overwrite=False): + """The box after ``labels`` are written over ``existing``, and what that wrote. + + A label of 0 leaves its voxel as it is. Without ``overwrite`` only + unannotated voxels (0) are written, as for a seed: what the user painted + stays. With it every voxel the labels cover is written, painted or not, + for a result the user has looked at and prefers to what is there. + Returns ``(after, (n_foreground, n_background, n_overwritten))``: the + box as it is then, of ``existing``'s dtype, or None when nothing would + change; how many voxels were written with foreground (2 and up) and with + background, and how many of those held a label before. + """ + labels = np.asarray(labels, dtype=existing.dtype) + if labels.shape != existing.shape: + raise ValueError(f"The labels are {labels.shape}; the box holds {existing.shape}") + if overwrite: + write = (labels > 0) & (labels != existing) + else: + write = (existing == 0) & (labels > 0) + n_overwritten = int(np.count_nonzero(write & (existing != 0))) + n_foreground = int(np.count_nonzero(write & (labels >= 2))) + counts = (n_foreground, int(np.count_nonzero(write)) - n_foreground, n_overwritten) + if not write.any(): + return None, counts + after = existing.copy() + after[write] = labels[write] + return after, counts + + +def rewrite(existing, labels): + """The box after ``labels`` are written over its foreground voxels, and how many changed. + + Only foreground voxels (2 and up) whose label changed are written, so a + stroke painted meanwhile elsewhere in the box is kept (Split Objects + relabels the box it read). Returns ``(after, n_changed)``, ``after`` + None when none changed. + """ + labels = np.asarray(labels, dtype=existing.dtype) + changed = (existing >= 2) & (labels != existing) + if not changed.any(): + return None, 0 + after = existing.copy() + after[changed] = labels[changed] + return after, int(np.count_nonzero(changed)) + + +def restore(current, before, after): + """The box put back to ``before`` where it still holds ``after``: an undo. + + Only voxels the action changed and nobody has touched since go back, so + a stroke painted in the box after the action is kept. Returns + ``(restored, n)``: the box then, or None when nothing goes back, and how + many voxels did. + """ + back = (before != after) & (current == after) + if not back.any(): + return None, 0 + restored = current.copy() + restored[back] = before[back] + return restored, int(np.count_nonzero(back)) diff --git a/cellmap_flow/portable/fill_calls.py b/cellmap_flow/portable/fill_calls.py new file mode 100644 index 00000000..07f2199e --- /dev/null +++ b/cellmap_flow/portable/fill_calls.py @@ -0,0 +1,121 @@ +"""What the browser calls for Label it's box tools: the functions +``web/src/backend/routes/view-labels.ts``'s Seed from Prediction, All +Background, Split Objects and Undo run through +``Inference.python("cellmap_flow.portable.fill_calls:", args, arrays)``, +in the shape ``chunkmirage.calls`` says: ``fn(args, arrays)``, ``args`` the +JSON arguments, ``arrays`` the bytes sent beside them, in order; a JSON +value, or a pair of JSON and arrays, back. Each decodes its arrays, calls +``fill.py`` and encodes the answer; the maths is not here, and the server's +routes (``dashboard/routes/finetune/view_labels.py``, through +``finetune/session/fill.py``) call the same functions around MinIO. + +The route reads the box from the paint store and writes back what comes +out, and keeps the undo stack, as the AI annotation's accept does +(``ai_annotate_calls.accept``): nothing here reads or keeps a volume. A box +travels as the store holds it: ``shape`` (z, y, x) and ``dtype`` (the +volume's) in ``args``, its bytes in C order, always ``arrays[0]``. A write +answers the box to write as its one array, or no array when nothing +changes, so the route writes nothing and pushes no undo entry then. + +A refusal the server answers 400 (new ids that do not fit in the volume's +dtype, a ValueError there) comes back as ``{"error": message}``, as +``label_it_calls`` answers its own, for the route to answer as the server +does. A ``ModuleNotFoundError`` (scipy, which Split Objects labels its +components with) is never caught: it is how chunkmirage's worker learns to +load scipy, before it calls again. +""" + +import numpy as np + +from cellmap_flow.portable import fill + + +def _box(args, data) -> np.ndarray: + """The box's labels from ``data``, its bytes as the paint store holds them.""" + return np.frombuffer(data, dtype=np.dtype(args["dtype"])).reshape(tuple(args["shape"])) + + +def _written(after, value): + """``value`` with the box to write beside it, or with no array when ``after`` is None.""" + return value, [] if after is None else [after] + + +def box(args, arrays): + """The annotation voxels a box in world nm covers (``fill.box_voxels``). + ``args``: ``volume`` (its ``output_voxel_size`` and + ``dataset_offset_nm``), ``offset_nm`` and ``shape_nm`` (the box's lower + corner and size, z, y, x) and ``volume_shape`` (the volume's voxels). + Answers ``{"lo", "hi"}``, or null when no voxel of the volume is in the + box.""" + voxels = fill.box_voxels(args["volume"], args["offset_nm"], args["shape_nm"], args["volume_shape"]) + if voxels is None: + return None + lo, hi = voxels + return {"lo": [int(v) for v in lo], "hi": [int(v) for v in hi]} + + +def _paint_counts(counts) -> dict: + n_foreground, n_background, n_overwritten = counts + return {"n_foreground": n_foreground, "n_background": n_background, "n_overwritten": n_overwritten} + + +def seed(args, arrays): + """A seed written into the box's unannotated voxels: ``fill.seed_labels`` + of the objects, written as ``fill.paint`` writes without overwrite. + ``args``: the box's ``shape`` and ``dtype``, and ``instance_target`` + (the model trains on instances, so new ids count up on a volume of more + than 8-bit ids: ``fill.counts_up``). ``arrays``: the box, then the + objects over it (uint32, 0 background, as ``label_it_calls.seed`` + answers them). Answers the counts (``n_foreground``, ``n_background``, + ``n_overwritten``) with the box to write; or the refusal, when the new + ids do not fit.""" + existing = _box(args, arrays[0]) + objects = np.frombuffer(arrays[1], dtype=np.uint32).reshape(existing.shape) + try: + labels = fill.seed_labels(objects, existing, count_up=fill.counts_up(args["instance_target"], existing.dtype)) + except ValueError as e: + return {"error": str(e)} + after, counts = fill.paint(existing, labels) + return _written(after, _paint_counts(counts)) + + +def background(args, arrays): + """Every unannotated voxel of the box background (1), written as + ``fill.paint`` writes without overwrite: All Background. ``args``: the + box's ``shape`` and ``dtype``; ``arrays``: the box. Answers as + ``seed``.""" + existing = _box(args, arrays[0]) + after, counts = fill.paint(existing, np.ones_like(existing)) + return _written(after, _paint_counts(counts)) + + +def split(args, arrays): + """The box's foreground relabelled by connected component + (``fill.relabel_objects``) and written over its foreground voxels + (``fill.rewrite``): Split Objects. ``args``: the box's ``shape`` and + ``dtype``, and ``connectivity`` and ``per_slice``, as the route has read + them. ``arrays``: the box. Answers the counts (``objects``, ``split``, + ``merged``) and ``changed``, the voxels whose label changed, with the box + to write; or the refusal, when the split-off pieces' new ids do not + fit.""" + existing = _box(args, arrays[0]) + try: + labels, counts = fill.relabel_objects( + existing, connectivity=int(args["connectivity"]), per_slice=bool(args["per_slice"]) + ) + except ValueError as e: + return {"error": str(e)} + after, changed = fill.rewrite(existing, labels) + return _written(after, {**{k: int(v) for k, v in counts.items()}, "changed": changed}) + + +def undo(args, arrays): + """The box put back to before an action where it still holds what the + action left (``fill.restore``): Undo. ``args``: the box's ``shape`` and + ``dtype``; ``arrays``: the box as it is now, then the undo entry's + ``before`` and ``after``, each shaped as the box. Answers ``restored``, + the voxels put back, with the box to write.""" + current = _box(args, arrays[0]) + before, after = _box(args, arrays[1]), _box(args, arrays[2]) + restored, n = fill.restore(current, before, after) + return _written(restored, {"restored": n}) diff --git a/cellmap_flow/portable/label_it.py b/cellmap_flow/portable/label_it.py new file mode 100644 index 00000000..d3360dc3 --- /dev/null +++ b/cellmap_flow/portable/label_it.py @@ -0,0 +1,334 @@ +"""Label it's segmentation of a prediction, written once for both modes. + +What the Finetune tab's *Seed from Prediction* makes of a model's output over +the box on screen: the request's options (``seed_options``), the channels a +seed reads (``seed_channels``; ``affinity_offsets`` for a model known by its +channel names alone), which served voxel lies under each annotation voxel +(``placement``, ``gather``), the output as a probability (``probability``) +and the objects of the *components* method (``components``: threshold, +connected components, smallest size, per slice). The server calls these +from ``dashboard/routes/finetune/view_labels.py`` around its read of the +model server's array; the browser from ``web/src/backend/routes/view-labels.ts`` +around the engine's ``readOutput``, through ``label_it_calls.py``. + +The mutex watershed is the one method not here: ``mwatershed`` is Rust, with +no Pyodide wheel, so each mode runs its own on what ``affinities`` gives +(``post.segment.mutex_watershed`` on the server, ``web/src/infer/affinity.ts`` +in the browser, parity-tested against it). The distance watershed and a +model's own instances are the server's alone (scikit-image, Cellpose); no +model the browser runs outputs either. + +scipy is imported inside the functions that need it, never at the top: the +browser's Python starts with numpy alone, and chunkmirage's worker loads a +package the first time a call fails to import it, then calls again, so the +first seed pays for scipy and opening a dataset does not. +""" + +import numpy as np + +# How a seed makes objects of the prediction (``post.segment``), each fitting +# a kind of output; which a model offers is each mode's to say +# (view_labels.methods_for, view-labels.ts methodsFor). +SEED_METHODS = ( + # Threshold, then an id per connected object: any output with a + # foreground threshold. The default. + "components", + # Affinity models: their offset channels, joined above the threshold. + "mutex_watershed", + # Distance models: touching objects cut apart where the distance dips. + "distance_watershed", + # A server that serves integer instance labels (Cellpose's masks). + "instances", +) + +# A model's output activation, as serving/probe.py observes it (and +# re-exports these from here, so the browser's Python has them too). +UNIT = "unit" # [0, 1] -- sigmoid/softmax already applied +SIGNED_UNIT = "signed_unit" # [-1, 1] -- tanh already applied +UNBOUNDED = "unbounded" # no output activation: logits, distances, affinities... + +# The connectivities in the order the forms list them, with their names. +CONNECTIVITIES = {1: "faces", 2: "faces and edges", 3: "all neighbours"} + + +def classify_output_range(lo: float, hi: float) -> str: + """Bucket an observed (min, max) output range into an activation class.""" + # Small tolerance: a saturating activation can land a hair outside its + # asymptote in float32. + eps = 1e-4 + if lo >= -eps and hi <= 1.0 + eps: + return UNIT + if lo >= -1.0 - eps and hi <= 1.0 + eps: + return SIGNED_UNIT + return UNBOUNDED + + +# --------------------------------------------------------------------------- +# The request +# --------------------------------------------------------------------------- + + +def as_connectivity(value, ndim=3) -> int: + """``value`` as scipy's connectivity for an ``ndim``-D array: 1 up to ``ndim``. + + Forms send strings, so "2" is 2. A value past ``ndim`` (3 for one 2D + slice) is ``ndim``: every neighbour, which is what it asked for. + Raises ValueError for anything that is not 1, 2 or 3, None among them. + """ + try: + connectivity = int(value) + except (TypeError, ValueError): + raise ValueError(f"connectivity must be 1, 2 or 3, got {value!r}") + if connectivity not in CONNECTIVITIES: + raise ValueError(f"connectivity must be 1, 2 or 3, got {value!r}") + return min(connectivity, ndim) + + +def as_bool(value) -> bool: + """A form's "true"/"false" as a bool; anything else by Python's truth.""" + if isinstance(value, str): + return value.strip().lower() in ("true", "1", "yes", "on") + return bool(value) + + +def seed_options(data) -> dict: + """A seed request's options, from its JSON body, with the defaults the + routes give an absent one: ``method`` ("components"), ``threshold`` (a + probability, 0.5, the model's own boundary), ``min_size`` (0), + ``connectivity`` (1, faces) and ``per_slice`` (false). + + Raises ValueError, which both modes answer 400, for a threshold outside + 0 to 1, a negative or non-integer size, a method not in SEED_METHODS + and a connectivity that is not 1, 2 or 3. A ``connectivity`` given as + null is refused with the rest, not read as absent: the key is there. + """ + threshold = data.get("threshold") + if threshold is not None and not 0 <= float(threshold) <= 1: + raise ValueError(f"threshold must be between 0 and 1, got {threshold}") + min_size = int(data.get("min_size") or 0) + if min_size < 0: + raise ValueError(f"min_size cannot be negative, got {min_size}") + method = data.get("method") or "components" + if method not in SEED_METHODS: + raise ValueError(f"method must be one of {', '.join(SEED_METHODS)}, got {method!r}") + return { + "method": method, + "threshold": 0.5 if threshold is None else float(threshold), + "min_size": min_size, + "connectivity": as_connectivity(data.get("connectivity", 1)), + "per_slice": as_bool(data.get("per_slice", False)), + } + + +def affinity_offsets(channel_names): + """The offsets of a model known by its channel names alone, or None + when they name no affinity channel. + + The server's rule for a model whose script says none + (``dashboard/routes/finetune/common.autodetect_output_type``): a model + with a channel named ``*_aff*`` is an affinity model, and its first + affinity channels, up to three, compare each voxel with the next one + along z, y and x in turn, whatever their names say after ``_aff``. + """ + n_aff = sum(1 for channel in channel_names or () if "_aff" in channel) + if not n_aff: + return None + return [[1 if axis == index else 0 for axis in range(3)] for index in range(min(n_aff, 3))] + + +def seed_channels(output_type, offsets, select_channel=None): + """The channels a seed reads, as a slice, or None for every one. + + As the trainer targets them: the selected channel; else an affinity + model's offset channels, the first ``len(offsets)`` (the rest, LSDs + say, are masked out of its loss); else every channel, which a binary + target is broadcast over. ``offsets`` is a list of the model's offsets. + A slice past the model's channels reads those there are + (``probability``). + """ + if select_channel is not None: + channel = int(select_channel) + if channel < 0: + raise ValueError(f"select_channel must be 0 or more, got {channel}") + return slice(channel, channel + 1) + if output_type == "affinities": + return slice(0, len(offsets)) + return None + + +# --------------------------------------------------------------------------- +# Where the prediction lies on the paint's grid +# --------------------------------------------------------------------------- + + +def placement(lo, hi, voxel_size, dataset_offset_nm, translation, declared, shape, drawn=None): + """Which voxel of a served array each annotation voxel of ``[lo, hi)`` + reads: the one its centre lies in, so a seed from a model served at + another voxel size (any served model can seed, not only the one the + volume is for) lies under that model's layer. + + The volume's grid is its ``voxel_size`` from ``dataset_offset_nm``, the + centre of its voxel 0 (its OME translation, as + ``finetune/session/volume.volume_corner_nm`` reads it; None is 0). The + served array's is its OME ``translation`` (voxel 0's centre too) and + ``declared`` voxel size, over ``shape`` voxels; ``drawn`` is the voxel + size Neuroglancer draws it at when that is another + (``viewer.layers.prediction_voxel_override``), which keeps the served + offset in voxels, so the array's corner moves with it. + + Returns ``(indices, lo, hi)``: per axis, the served index of each + annotation voxel the array covers, and the box shrunk to those voxels; + None when it covers none along some axis. + """ + voxel = np.asarray(voxel_size, dtype=float) + offset = np.zeros(3) if dataset_offset_nm is None else np.asarray(dataset_offset_nm, dtype=float) + volume_corner = offset - voxel / 2 + declared = np.asarray(declared, dtype=float) + drawn = declared if drawn is None else np.asarray(drawn, dtype=float) + served_corner = (np.asarray(translation, dtype=float) - declared / 2) / declared * drawn + indices, new_lo, new_hi = [], [int(v) for v in lo], [int(v) for v in hi] + for axis in range(3): + centres = volume_corner[axis] + (np.arange(new_lo[axis], new_hi[axis]) + 0.5) * voxel[axis] + index = np.floor((centres - served_corner[axis]) / drawn[axis]).astype(int) + # One run: the centres rise along the axis. + inside = np.flatnonzero((index >= 0) & (index < int(shape[axis]))) + if not inside.size: + return None + new_lo[axis], new_hi[axis] = new_lo[axis] + int(inside[0]), new_lo[axis] + int(inside[-1]) + 1 + indices.append(index[inside]) + return indices, new_lo, new_hi + + +def read_box(indices): + """``(lo, hi)``: the box of the served array the indices span, the one to read for ``gather``.""" + return [int(i[0]) for i in indices], [int(i[-1]) + 1 for i in indices] + + +def gather(region, indices): + """The prediction on the annotation grid: each annotation voxel the + served voxel ``placement`` gave it. ``region`` is (channels, z, y, x), + the served box ``read_box`` gives; with the two grids equal, all of it + in order.""" + return region[(slice(None),) + np.ix_(*(np.asarray(i) - i[0] for i in indices))] + + +# --------------------------------------------------------------------------- +# The prediction as a probability +# --------------------------------------------------------------------------- + + +def _sigmoid(x): + # 1 / (1 + e^-x) in the input's float32. numpy's exp overflows to inf + # below about -88, and 1 / inf is the 0 the sigmoid tends to there. + # numpy's own, not scipy's expit: a seed by the mutex watershed and the + # browser's rank read probabilities, and need no scipy for it. + with np.errstate(over="ignore"): + return 1 / (1 + np.exp(-x)) + + +def probability(prediction, output_class=None, channels=None): + """The prediction as a probability, (channels, z, y, x), 0.5 at the model's decision boundary. + + The boundary depends on the output's activation: 0.5 on [0, 1] output + (a sigmoid already applied; the cellmap distance models end in one, and + 0.5 is their object boundary), 0 on tanh's [-1, 1], and 0 on unbounded + output, logits or a signed distance alike. Each is turned into a + probability, so ``channels`` (a slice: the affinity channels, say) can + be averaged and one threshold means the same for every model. A slice + running past the model's channels reads those there are; one starting + past them is refused. ``output_class`` is the server's + (serving.probe), or read off the values when it has none. + """ + if channels is not None: + prediction = prediction[channels] + if not prediction.shape[0]: + raise ValueError(f"The model has no channel {channels.start}") + prediction = prediction.astype(np.float32, copy=False) + output_class = output_class or classify_output_range(float(prediction.min()), float(prediction.max())) + if output_class == UNIT: + return prediction + if output_class == SIGNED_UNIT: + return (prediction + 1) / 2 + return _sigmoid(prediction) + + +def foreground_mask(prediction, output_class=None, channels=None, threshold=0.5): + """Where the model says foreground: its ``probability``, averaged over ``channels``, over ``threshold``. + + ``threshold`` is in probability whatever the activation: 0.5 is the + model's own boundary. Strictly over: a voxel at it is background. + """ + return probability(prediction, output_class, channels).mean(axis=0) > threshold + + +def affinities(prediction, output_class, offsets): + """The mutex watershed's input: the probability of an affinity model's + offset channels, the first ``len(offsets)`` (as ``seed_channels`` + takes them; fewer when the model has fewer).""" + return probability(prediction, output_class, slice(0, len(offsets))) + + +# --------------------------------------------------------------------------- +# The objects +# --------------------------------------------------------------------------- + + +def _structure(ndim, connectivity): + from scipy.ndimage import generate_binary_structure + + return generate_binary_structure(ndim, as_connectivity(connectivity, ndim)) + + +def remove_small(labels, min_size=0): + """``labels`` with objects of fewer than ``min_size`` voxels made background. + + The rest are numbered 1..n again, in the order they had. ``labels`` + must already be numbered without large gaps (any segmenter's output): + the sizes are counted per id. + """ + labels = np.asarray(labels) + if min_size is None or int(min_size) <= 1 or not labels.any(): + return labels.astype(np.uint64, copy=False) + sizes = np.bincount(labels.ravel().astype(np.intp)) + keep = sizes >= int(min_size) + keep[0] = False + new_ids = np.zeros(sizes.size, dtype=np.uint64) + new_ids[keep] = np.arange(1, np.count_nonzero(keep) + 1, dtype=np.uint64) + return new_ids[labels.astype(np.intp)] + + +def connected_components(mask, connectivity=1, min_size=0, per_slice=False): + """An id per connected object of ``mask`` (its nonzero voxels): uint64, + 0 for background and 1..n with no id skipped, numbered as + ``scipy.ndimage.label`` numbers them. + + ``per_slice``: each z slice of a (z, y, x) mask is labelled on its own, + in 2D, its ids following the slice before's. For data whose objects are + 2D (what Cellpose sees), or annotations painted slice by slice: an + object touching itself across slices is still one per slice. + ``min_size``: objects of fewer voxels are background (specks a + threshold let through); per slice, each slice's piece is counted alone. + """ + from scipy.ndimage import label + + mask = np.asarray(mask) != 0 + if per_slice: + if mask.ndim != 3: + raise ValueError(f"per_slice labels (z, y, x) masks, got shape {mask.shape}") + structure = _structure(2, connectivity) + labels = np.zeros(mask.shape, dtype=np.uint64) + count = 0 + for z, plane in enumerate(mask): + plane_labels, n = label(plane, structure) + labels[z] = np.where(plane_labels > 0, plane_labels.astype(np.uint64) + np.uint64(count), 0) + count += n + else: + labels, _ = label(mask, _structure(mask.ndim, connectivity)) + return remove_small(labels, min_size) + + +def components(prediction, output_class, channels, threshold=0.5, min_size=0, connectivity=1, per_slice=False): + """The objects of the *components* method: the ``foreground_mask`` of + the prediction over ``channels``, an id per connected object + (``connected_components``).""" + return connected_components(foreground_mask(prediction, output_class, channels, threshold), connectivity, min_size, per_slice) diff --git a/cellmap_flow/portable/label_it_calls.py b/cellmap_flow/portable/label_it_calls.py new file mode 100644 index 00000000..a8ff54ae --- /dev/null +++ b/cellmap_flow/portable/label_it_calls.py @@ -0,0 +1,117 @@ +"""What the browser calls for Label it: the functions +``web/src/backend/routes/view-labels.ts`` (and the rank's +``web/src/backend/routes/rank.ts``, for the placement and the probability) +runs through ``Inference.python("cellmap_flow.portable.label_it_calls:", +args, arrays)``, in the shape ``chunkmirage.calls`` says: ``fn(args, arrays)``, +``args`` the JSON arguments, ``arrays`` the bytes sent beside them; a JSON +value, or a pair of JSON and arrays, back. Each decodes its arrays, calls +``label_it`` and encodes the answer; the maths is not here. + +A refusal the server answers 400 (a ValueError there) comes back as +``{"error": message}``, for the route to answer as the server does; any +other exception rejects the call, the route's 500. A ``ModuleNotFoundError`` +is never caught: it is how chunkmirage's worker learns to load scipy, before +it calls again. +""" + +import numpy as np + +from cellmap_flow.portable import label_it + + +def _slice(channels): + """The channels as ``label_it`` takes them, from ``[start, stop]`` or null.""" + return None if channels is None else slice(int(channels[0]), int(channels[1])) + + +def _placed(args): + """``label_it.placement`` of ``args``' box (``lo``, ``hi``) of ``volume`` + (its ``output_voxel_size`` and ``dataset_offset_nm``) on ``level`` (the + served level: ``origin``, its OME translation, ``voxel`` and ``shape``), + as JSON: the box shrunk, the indices and the served box to read for + them; null when the level covers none of it.""" + volume, level = args["volume"], args["level"] + placed = label_it.placement( + args["lo"], args["hi"], volume["output_voxel_size"], volume.get("dataset_offset_nm"), + level["origin"], level["voxel"], level["shape"], + ) + if placed is None: + return None + indices, lo, hi = placed + read_lo, read_hi = label_it.read_box(indices) + return {"lo": lo, "hi": hi, "indices": [i.tolist() for i in indices], "read_lo": read_lo, "read_hi": read_hi} + + +def seed_options(args, arrays): + """The seed's options (``label_it.seed_options``) from ``args``, the + request's body: ``{"options": ...}``, or the refusal.""" + try: + return {"options": label_it.seed_options(args)} + except ValueError as e: + return {"error": str(e)} + + +def seed_plan(args, arrays): + """What a seed reads of the model, and where. ``args``: the model's + ``channel_names``, the request's ``select_channel`` (null when it gives + none), and the box as ``place`` takes it. Answers ``channels`` (``[start, + stop]``, or null for every one), ``offsets`` (an affinity model's, by + ``label_it.affinity_offsets``, else null) and ``placement`` (``place``'s + answer); or the refusal of the selected channel.""" + offsets = label_it.affinity_offsets(args["channel_names"]) + try: + channels = label_it.seed_channels("affinities" if offsets else "binary", offsets, args.get("select_channel")) + except ValueError as e: + return {"error": str(e)} + return { + "channels": None if channels is None else [channels.start, channels.stop], + "offsets": offsets, + "placement": _placed(args), + } + + +def place(args, arrays): + """Which served voxel lies under each voxel of the box (``_placed``): + ``args`` ``lo``, ``hi``, ``volume`` and ``level``.""" + return _placed(args) + + +def seed(args, arrays): + """A seed's objects, or the mutex watershed's input. ``arrays``: the + served region ``seed_plan``'s placement says to read (C, z, y, x + float32, its ``shape`` in ``args``). ``args`` also: the ``indices`` of + that placement, ``method`` and the options (``seed_options``'), the + model's ``output_class`` (null: read off the values), and the plan's + ``channels`` and ``offsets``. + + *components*: ``{"count"}`` and the objects, (z, y, x) uint32 over the + box. *mutex_watershed*: ``{"shape"}`` and ``label_it.affinities``, float32, + which the browser's mutex watershed takes from there. A refusal (a + channel the model does not have) is ``{"error"}``. + """ + region = np.frombuffer(arrays[0], np.float32).reshape(args["shape"]) + prediction = label_it.gather(region, args["indices"]) + output_class = args.get("output_class") + try: + if args["method"] == "components": + objects = label_it.components( + prediction, output_class, _slice(args.get("channels")), args["threshold"], args["min_size"], + args["connectivity"], args["per_slice"], + ) + return {"count": int(objects.max(initial=0))}, [objects.astype(np.uint32)] + if args["method"] == "mutex_watershed": + if not args.get("offsets"): + raise ValueError("The model's affinity offsets are unknown, so it cannot seed by mutex_watershed") + out = label_it.affinities(prediction, output_class, args["offsets"]) + return {"shape": list(out.shape)}, [np.ascontiguousarray(out, dtype=np.float32)] + except ValueError as e: + return {"error": str(e)} + raise ValueError(f"the browser seeds by components or mutex_watershed, not {args['method']!r}") + + +def probability(args, arrays): + """``label_it.probability`` of a block (``arrays``: C, z, y, x float32, + its ``shape`` in ``args``) by the model's ``output_class`` (null: read + off the values), every channel: the same shape, float32.""" + block = np.frombuffer(arrays[0], np.float32).reshape(args["shape"]) + return np.ascontiguousarray(label_it.probability(block, args.get("output_class")), dtype=np.float32) diff --git a/cellmap_flow/portable/regions.py b/cellmap_flow/portable/regions.py new file mode 100644 index 00000000..af9d3a68 --- /dev/null +++ b/cellmap_flow/portable/regions.py @@ -0,0 +1,144 @@ +"""Good regions on numbers alone: the region a mark makes, its size, which +stored entries are regions, and where a region anchors the model on a +volume's grid, so that the server and the browser make, draw and place a +region the same way. ``dashboard/routes/finetune/good_regions.py`` says what +good regions are for; its routes and the server's trainer +(``finetune/data/sampler.py``) call these in their process, and the +browser's routes and trainer (``web/src/backend/routes/good-regions.ts``, +``web/src/finetune/run.ts``) call them through ``volume_calls.py``. + +A region is ``{"id", "label", "offset_nm", "shape_nm"}``: a box in world nm, +z, y, x, its lower corner and its size, as ``good_regions.json`` keeps it. +""" + +import math +import uuid +from collections.abc import Mapping +from numbers import Real +from typing import Optional + +import numpy as np + +# Used when neither the request nor the volume says how big a region +# should be. One model *output* patch is the natural unit: it is what you can +# actually see and judge on screen, and it is the region a training patch +# computes loss over. Sizing to the input field of view instead would claim +# the model is right across 2848nm when you only looked at the middle 896nm +# of it -- and would not match annotation crops, which are already write_shape. +DEFAULT_REGION_SIZE_NM = 896.0 + + +def _is_number(value) -> bool: + return isinstance(value, Real) and not isinstance(value, bool) + + +def _triple(value, positive: bool) -> Optional[np.ndarray]: + """``value`` as three finite floats (each positive with ``positive``), or None.""" + if not isinstance(value, (list, tuple, np.ndarray)) or len(value) != 3: + return None + if not all(_is_number(v) and math.isfinite(v) and (v > 0 or not positive) for v in value): + return None + return np.asarray(value, dtype=float) + + +def default_size_nm(output_size=None, output_voxel_size=None) -> list: + """One model output patch of a volume, in nm (see DEFAULT_REGION_SIZE_NM): + ``output_size`` voxels of ``output_voxel_size`` nm, or 896 nm a side when + either is missing or not numbers.""" + if output_size and output_voxel_size: + try: + return (np.asarray(output_size, dtype=float) * np.asarray(output_voxel_size, dtype=float)).tolist() + except (TypeError, ValueError): + pass + return [DEFAULT_REGION_SIZE_NM] * 3 + + +def region_size_nm(size_nm, output_size=None, output_voxel_size=None) -> list: + """The size a mark asks for, in nm, z, y, x. + + No size (None, 0, an empty list) is one output patch of the volume + (``default_size_nm`` of ``output_size`` and ``output_voxel_size``); + one number, alone or in a list, is the same along every axis; else three + numbers. ValueError ("size_nm must be positive") for anything else: a + size that is not a number, not finite or not positive, or of another + length. + """ + if not size_nm: + return default_size_nm(output_size, output_voxel_size) + values = list(size_nm) if isinstance(size_nm, (list, tuple)) else [size_nm] + if len(values) == 1: + values = values * 3 + size = _triple(values, positive=True) + if size is None: + raise ValueError("size_nm must be positive") + return size.tolist() + + +def new_region(centre_nm, size_nm, count: int, label=None, region_id: Optional[str] = None) -> dict: + """The region a mark makes: a box of ``size_nm`` centred on ``centre_nm``, + labelled ``label`` (a non-empty string), or ``good-`` after + ``count`` regions; its id eight hex digits unless ``region_id`` is given.""" + centre = np.asarray(centre_nm, dtype=float) + size = np.asarray(size_nm, dtype=float) + return { + "id": region_id or uuid.uuid4().hex[:8], + "label": label if isinstance(label, str) and label else f"good-{count + 1}", + "offset_nm": (centre - size / 2).tolist(), + "shape_nm": size.tolist(), + } + + +def region_box_nm(region) -> Optional[tuple]: + """``(offset_nm, shape_nm)`` of a stored region, as float arrays, or None + when the entry is not a region: a mapping whose offset_nm and shape_nm + are three finite numbers each, its shape positive. Its id and label are + not needed to place it.""" + if not isinstance(region, Mapping): + return None + offset, shape = _triple(region.get("offset_nm"), positive=False), _triple(region.get("shape_nm"), positive=True) + return None if offset is None or shape is None else (offset, shape) + + +def region_centre_voxels(region, corner_nm, voxel_nm, shape_voxels) -> tuple: + """Where a region's centre lies on a volume's grid: ``(where, centre)``. + + ``corner_nm`` is the volume's voxel-0 lower corner (``volume_corner_nm``), + ``voxel_nm`` its output voxel size and ``shape_voxels`` its shape. The + centre is in voxels from the corner (voxel v spans [v, v + 1)). ``where`` + is "malformed" (not a region, ``region_box_nm``; the centre is None), + "outside" (a region marked on another volume, which would anchor the + model to padding) or "inside". + """ + box = region_box_nm(region) + if box is None: + return "malformed", None + offset, shape = box + centre = (offset + shape / 2 - np.asarray(corner_nm, dtype=float)) / np.asarray(voxel_nm, dtype=float) + if np.any(centre < 0) or np.any(centre >= np.asarray(shape_voxels)): + return "outside", centre + return "inside", centre + + +def anchor_boxes(regions, corner_nm, voxel_nm, shape_voxels, block) -> dict: + """The box of the volume's voxels each region anchors, and which were skipped. + + The box is the rehearsal patch the server's trainer cuts for the region: + one output ``block`` whose start is the region's centre snapped so that + the block is whole voxels (``PatchReader.snap``: ``floor(c - block/2 + + 1/2)``), clipped to the volume. Answers ``{"boxes": [{"region", "lo", + "hi"}], "malformed": [...], "outside": [...]}``, each region by its + index in ``regions`` and each box [lo, hi) in voxels, z, y, x. + """ + boxes, malformed, outside = [], [], [] + shape = np.asarray(shape_voxels, dtype=int) + size = np.asarray(block, dtype=int) + for index, region in enumerate(regions): + where, centre = region_centre_voxels(region, corner_nm, voxel_nm, shape) + if where != "inside": + (malformed if where == "malformed" else outside).append(index) + continue + # A centre inside the volume leaves some of its block inside too. + start = np.floor(centre - size / 2 + 0.5).astype(int) + lo, hi = np.maximum(start, 0), np.minimum(start + size, shape) + boxes.append({"region": index, "lo": lo.tolist(), "hi": hi.tolist()}) + return {"boxes": boxes, "malformed": malformed, "outside": outside} diff --git a/cellmap_flow/portable/targets.py b/cellmap_flow/portable/targets.py new file mode 100644 index 00000000..2aca3323 --- /dev/null +++ b/cellmap_flow/portable/targets.py @@ -0,0 +1,284 @@ +"""The paint as training targets: what a finetune trains a model toward. + +Labels mean what the paint layer paints: 0 unpainted (no loss), 1 +background, 2 and up objects (an instance volume numbers each one). Each +function takes the labels of a patch, (Z, Y, X), or of a batch of them with +any leading axes, (..., Z, Y, X), and answers float32 arrays with a channel +axis in front of the three spatial ones, (..., C, Z, Y, X): one channel per +output channel of the model, the target and the mask of the voxels its loss +counts. The server's ``finetune/target_transforms.py`` wraps these for the +trainer's (B, 1, Z, Y, X) tensors; the browser calls them on one painted +chunk through ``targets_calls.py``. + +What a model trains toward is the caller's to say: the binary target for a +model of objects, the affinities along ``offsets`` for an affinity model +(the server reads them from the model's script or its channel names, the +browser from the names in its manifest), and for a distance model the soft +signed distance (dense paint) or the bounds on it (scribbles), on the +server only for now. +""" + +import numpy as np + +# How far out, in sigma, a distance bound still says anything: tanh(3) is +# 0.995, so a voxel known to be at least 3 sigma inside is as good as at 3 +# sigma, and an upper bound beyond it constrains nothing a threshold sees. +SATURATION_SIGMAS = 3.0 + + +def binary(labels, channels: int = 1): + """``(target, mask)``: 1 where the voxel is an object (label 2 or more), + and where it is painted (label above 0), the same on every one of + ``channels`` channels. + + One channel is the target of a model of one class. More treat a + multi-channel model (affinities, distances) as simple binary + segmentation, every channel trained toward the same objects. + """ + labels = np.asarray(labels) + _check_patch(labels) + _check_channels(channels) + shape = (*labels.shape[:-3], channels, *labels.shape[-3:]) + target = np.broadcast_to((labels > 1)[..., None, :, :, :], shape).astype(np.float32) + mask = np.broadcast_to((labels > 0)[..., None, :, :, :], shape).astype(np.float32) + return target, mask + + +def affinity(labels, offsets, channels: int = None): + """``(target, mask)``: channel c the affinity along ``offsets[c]``. + + A voxel's affinity is 1 when it and its neighbour at the offset are the + same object (equal labels above 1), and 0 when they are different + objects or either is background. It is written at the voxel, and masked + only where both of the pair are painted, so each channel has a mask of + its own. + + ``offsets`` are [dz, dy, dx], one for each of the first channels; an + offset of None leaves its channel out of the loss, as are the channels + past the offsets when ``channels`` (the model's output channels, by + default one per offset) has more: LSDs, say, which then get no + gradient. An offset as long as the patch along its axis or longer pairs + no voxel inside it, so its channel has an empty mask: a one-slice + patch's z channel, the head of a 2D student, keeps the model's weights. + """ + labels = np.asarray(labels) + _check_patch(labels) + channels = len(offsets) if channels is None else channels + _check_channels(channels) + if len(offsets) > channels: + raise ValueError(f"{len(offsets)} affinity offsets for {channels} channel(s): a model has a channel per offset") + spatial = labels.shape[-3:] + target = np.zeros((*labels.shape[:-3], channels, *spatial), np.float32) + mask = np.zeros_like(target) + for c, offset in enumerate(offsets): + if offset is None: + continue + pairs = _pairs(spatial, offset) + if pairs is None: + continue + here, there = pairs + a, b = labels[(..., *here)], labels[(..., *there)] + target[(..., c, *here)] = (a == b) & (a > 1) + mask[(..., c, *here)] = (a > 0) & (b > 0) + return target, mask + + +def _pairs(shape, offset): + """The voxels whose neighbour at ``offset`` lies in a patch of ``shape``, + and those neighbours, as two tuples of slices; None when there are none. + + Along each axis the voxels are [max(0, -d), min(n, n - d)) and their + neighbours d further on. An offset of n or more along an axis of n + voxels pairs nothing, which is said here rather than left to slicing, + where a stop of n - d below 0 counts from the far end. + """ + if len(offset) != 3 or any(int(d) != d for d in offset): + raise ValueError(f"an affinity offset is three whole numbers (dz, dy, dx), not {offset!r}") + if any(abs(int(d)) >= n for d, n in zip(offset, shape)): + return None + here = tuple(slice(max(0, -int(d)), min(n, n - int(d))) for d, n in zip(offset, shape)) + there = tuple(slice(max(0, int(d)), min(n, n + int(d))) for d, n in zip(offset, shape)) + return here, there + + +def distance(labels, sigma_voxels: float = 6.0, channels: int = 1): + """``(target, mask)``: the soft signed distance of models trained the fly_organelles way. + + The target is ``(tanh((edt(fg) - edt(not fg)) / sigma) + 1) / 2`` with + distances in voxels: 0.5 on the object boundary, rising to 1 inside and + falling to 0 outside, saturating a few sigma away. That is exactly what + ``fly_organelles.utils.Distance`` fed the cellmap distance models (the + nuc/mito "*_distance_*" HuggingFace repos use sigma=6), so sigmoid(model + output) is comparable to this target voxel for voxel. It goes with a + BCE-with-logits loss, which accepts soft targets; margin and dice do not. + The target is the same on every one of ``channels`` channels. + + Masking: an EDT computed inside a patch only sees boundaries inside the + patch. For an annotated voxel the computed |d| is an upper bound on the + truth -- the real nearest boundary may sit just past the patch edge or + inside an unannotated pocket. A voxel is supervised only when its + computed |d| is no larger than its distance to the nearest unannotated + voxel or patch edge (so no unseen boundary can be closer), or when that + distance is already past 3*sigma, where tanh has saturated and the exact + value no longer matters. + """ + labels = np.asarray(labels) + _check_patch(labels) + _check_channels(channels) + sigma = _positive_sigma(sigma_voxels) + lead, spatial = labels.shape[:-3], labels.shape[-3:] + target = np.empty((*lead, 1, *spatial), np.float32) + mask = np.empty_like(target) + for at in np.ndindex(*lead): + target[(*at, 0)], mask[(*at, 0)] = _distance_one(labels[at], sigma) + shape = (*lead, channels, *spatial) + return np.broadcast_to(target, shape).copy(), np.broadcast_to(mask, shape).copy() + + +def _distance_one(ann, sigma): + """(Z, Y, X) labels -> (target, mask) float32, for ``distance``.""" + from scipy.ndimage import distance_transform_edt as edt + + annotated = ann > 0 + fg = ann >= 2 + bg = ann == 1 + target = np.zeros(ann.shape, dtype=np.float32) + mask = np.zeros(ann.shape, dtype=np.float32) + if not annotated.any(): + return target, mask + + # edt(x) is the distance from each nonzero voxel of x to the nearest + # zero. With no zero anywhere scipy returns a large finite number for + # every voxel; treat that as "no boundary in this patch" explicitly. + # + # Both sides measure to the nearest *known* voxel of the other class: + # a background voxel's distance to annotated foreground, and a + # foreground voxel's distance to annotated background. d_in used to + # be edt(fg), which counts unannotated voxels as background too, so a + # foreground voxel next to an unannotated one -- at the edge of a + # dense crop that cuts through an object, say -- got a small d_in that + # always passed the trust test below, and was supervised toward 0.58 + # to 0.88 as though the object ended at the crop edge. + d_in = edt(~bg) if bg.any() else np.full(ann.shape, np.inf) + d_out = edt(~fg) if fg.any() else np.full(ann.shape, np.inf) + signed = np.where(fg, d_in, -d_out) + + # Distance from each annotated voxel to the nearest unannotated voxel + # or to just outside the patch (the one-voxel pad of False). + known = np.pad(annotated, 1, constant_values=False) + trust = edt(known)[1:-1, 1:-1, 1:-1] + + reliable = np.abs(signed) <= trust + saturated = trust >= 3.0 * sigma + mask[annotated & (reliable | saturated)] = 1.0 + target[:] = (np.tanh(signed / sigma) + 1.0) / 2.0 + return target, mask + + +def interval(labels, sigma_voxels: float = 6.0, voxel_size_nm=None): + """``(bounds, mask)``: bounds on a distance model's output at each painted voxel, for scribbles. + + A distance target needs every voxel's distance to the object boundary, + and a stroke does not say where the boundary is. It does bound it + (``distance_bounds``). The bounds are returned in the model's logit + space, where the distance target (tanh(d/sigma) + 1)/2 = sigmoid(2d/sigma) + is the line z = 2d/sigma, with the sign of the painted class. Past + SATURATION_SIGMAS a lower bound is capped there and an upper bound + dropped (+-inf). + + Distances are Euclidean in nm (``voxel_size_nm``, so an anisotropic + grid is measured as it is; 1 nm a voxel when None), and sigma, which is + in output voxels as for the distance target, is taken in the finest + axis's voxels (``sigma_nm``). + + bounds has two channels, the lower and the upper bound on the logit (0 + off the paint), and mask one, the painted voxels, whatever the model's + channels: losses.IntervalLoss broadcasts them over its output. + """ + labels = np.asarray(labels) + _check_patch(labels) + voxel_size = _voxel_size(voxel_size_nm) + sigma = sigma_nm(sigma_voxels, voxel_size) + lead, spatial = labels.shape[:-3], labels.shape[-3:] + bounds = np.empty((*lead, 2, *spatial), np.float32) + mask = np.empty((*lead, 1, *spatial), np.float32) + for at in np.ndindex(*lead): + ann = labels[at] + lower, upper = distance_bounds(ann, voxel_size) + logit_per_nm = 2.0 / sigma + saturation = SATURATION_SIGMAS * sigma + near = logit_per_nm * np.minimum(lower, saturation) + far = np.where(upper < saturation, logit_per_nm * upper, np.inf) + fg, bg = ann >= 2, ann == 1 + # Background lies on the negative side: its bounds swap and change sign. + bounds[(*at, 0)] = np.where(fg, near, np.where(bg, -far, 0.0)) + bounds[(*at, 1)] = np.where(fg, far, np.where(bg, -near, 0.0)) + mask[(*at, 0)] = ann > 0 + return bounds, mask + + +def sigma_nm(sigma_voxels: float, voxel_size_nm=None) -> float: + """The interval target's sigma in nm: ``sigma_voxels`` of the finest axis's voxels.""" + return _positive_sigma(sigma_voxels) * min(_voxel_size(voxel_size_nm)) + + +def distance_bounds(ann, voxel_size_nm=None): + """(L, U) in nm for each voxel of a (Z, Y, X) annotation; 0 off the paint, U inf with no other class. + + - upper, U: the nearest voxel painted as the other class. The boundary + lies between the two, so |d| <= U. + - lower, L: the nearest voxel not painted as the voxel's own class -- + the other class, unpainted (it may be either), or past the patch edge. + Every voxel closer is known to be its own class, so |d| >= L. + + Where the paint is dense, the nearest voxel not of its own class is of + the other, and L == U: the exact distance ``distance`` would give, with + the same convention (edt to the nearest voxel of the other class, so 1 + voxel on either side of a boundary). On a stroke the two bracket the + truth. Both are computed in the patch the trainer gets, which keeps them + valid: a closer other-class voxel outside the patch would only lower the + true |d|, which U already exceeds or meets, and the patch edge counts as + "not its own class", which can only lower L. Reading context around the + patch would tighten bounds near its edges, but the dataset hands the + trainer the patch alone (augmented with it), so they are left as loose + as the patch makes them. + """ + from scipy.ndimage import distance_transform_edt as edt + + ann = np.asarray(ann) + sampling = _voxel_size(voxel_size_nm) + fg, bg = ann >= 2, ann == 1 + lower = np.zeros(ann.shape) + upper = np.zeros(ann.shape) + for own, other in ((fg, bg), (bg, fg)): + if not own.any(): + continue + # edt(x) is the distance from each nonzero voxel of x to the + # nearest zero; with no zero at all scipy returns garbage, so a + # patch without the other class has no upper bound, explicitly. + to_other = edt(~other, sampling=sampling) if other.any() else np.inf + # Padded with a ring of "not own class": the patch's surroundings. + to_not_own = edt(np.pad(own, 1, constant_values=False), sampling=sampling)[1:-1, 1:-1, 1:-1] + upper[own] = np.broadcast_to(to_other, ann.shape)[own] + lower[own] = to_not_own[own] + return lower, upper + + +def _check_patch(labels): + if labels.ndim < 3: + raise ValueError(f"labels are a patch (z, y, x), or a batch of them, not an array of shape {labels.shape}") + + +def _check_channels(channels): + if int(channels) != channels or channels < 1: + raise ValueError(f"a target needs at least one channel, not {channels!r}") + + +def _positive_sigma(sigma_voxels) -> float: + if sigma_voxels <= 0: + raise ValueError(f"sigma_voxels must be positive, got {sigma_voxels}") + return float(sigma_voxels) + + +def _voxel_size(voxel_size_nm) -> tuple: + return tuple(float(v) for v in ((1.0, 1.0, 1.0) if voxel_size_nm is None else voxel_size_nm)) diff --git a/cellmap_flow/portable/targets_calls.py b/cellmap_flow/portable/targets_calls.py new file mode 100644 index 00000000..0d27fe10 --- /dev/null +++ b/cellmap_flow/portable/targets_calls.py @@ -0,0 +1,34 @@ +"""What the browser calls for training targets: the function +``web/src/finetune/target-call.ts`` runs through chunkmirage's +``Engine.call("cellmap_flow.portable.targets_calls:targets", args, arrays)`` +(``calls.py`` says the shape). It decodes the chunk's labels, calls the maths +in ``targets.py`` and encodes the answer; the maths is not here. +""" + +import numpy as np + +from cellmap_flow.portable import targets as maths +from cellmap_flow.portable.label_it import affinity_offsets + + +def targets(args, arrays): + """A painted chunk's target and mask (``targets.binary``, or + ``targets.affinity`` for an affinity model). ``args``: ``shape`` + (z, y, x), ``labels_dtype``, ``channels`` (the model's output channels), + and either ``offsets`` (null for a binary model, else one [dz, dy, dx] + or null per channel from the first, as a caller that knows them gives + them) or ``channel_names``, from which the server's rule decides + (``label_it.affinity_offsets``: a model with a ``*_aff*`` channel is an + affinity model, its first affinity channels along z, y and x), so the + browser, which has only its manifest's names, trains on what the server + would. ``arrays``: the chunk's labels (z, y, x). Answers the channels + with the target and the mask, each float32 (channels, z, y, x).""" + labels = np.frombuffer(arrays[0], np.dtype(args["labels_dtype"])).reshape(tuple(args["shape"])) + channels, offsets = int(args["channels"]), args.get("offsets") + if offsets is None and args.get("channel_names"): + offsets = affinity_offsets(args["channel_names"]) + if offsets is None: + target, mask = maths.binary(labels, channels) + else: + target, mask = maths.affinity(labels, offsets, channels) + return {"channels": channels}, [target, mask] diff --git a/cellmap_flow/portable/volume.py b/cellmap_flow/portable/volume.py new file mode 100644 index 00000000..b6e26c53 --- /dev/null +++ b/cellmap_flow/portable/volume.py @@ -0,0 +1,164 @@ +"""Where a new annotation volume lies and what holds its labels: the planning +of ``finetune/session/volume.py`` (its module docstring says what a volume +is), on numbers alone, so that the server and the browser plan a volume the +same way. The server reads the raw level and the model's geometry +(``ImageDataInterface``, ``resolve_model_geometry``) and the browser its +``DatasetInfo`` and model manifest (``web/src/finetune/volume.ts``, through +``volume_calls.py``); both hand what they found to ``plan_volume`` here. + +The grid. A model reads one level of the raw, the one ``select_level``'s +"floor" picks for its input voxel size (or, with Resample, the raw resampled +to that size), and both servers serve its prediction on that level's grid: +output voxel 0 starts at the level's corner, and an output voxel is the +level's voxel times the model's ratio of output to input voxel size. The +volume lies on that grid, one chunk per model output block, so that a chunk +of paint is a block of the prediction it is painted over, and it covers the +level's whole extent, a partial output voxel at the far end rounded up, then +padded to whole chunks. +""" + +import math +from collections.abc import Mapping + +import numpy as np + +# The training targets whose annotation volumes hold an id per object +# rather than foreground and background alone. +INSTANCE_TARGETS = ("affinities", "flows") + +# Within this many voxels of a whole number, a block size in nm divided by its +# voxel size is that number: the float noise of nm (10.48 nm voxels) is not a +# fraction of a voxel. +WHOLE_VOXEL_TOLERANCE = 1e-6 + + +def annotation_dtype(output_type) -> str: + """The labels' dtype of a new volume for a model that trains on ``output_type``. + + uint8 (ids up to 255) for a model trained on foreground and background; + uint16 (up to 65535) for one trained on instances (affinities, Cellpose's + flows), whose seeds number each object: 282 of a Cellpose model's masks + in one box did not fit a uint8 volume. + """ + return "uint16" if output_type in INSTANCE_TARGETS else "uint8" + + +def volume_corner_nm(dataset_offset_nm, output_voxel_size) -> np.ndarray: + """The world position of an annotation volume's voxel-0 lower corner, in nm. + + ``dataset_offset_nm`` (a root attr of every volume) is also written as the + volume's OME-NGFF translation, and a translation is voxel 0's *centre*. + Neuroglancer drew the volume that way while it was painted, so that is + where the labels are: read as a corner, the value puts every label half + an annotation voxel away from where it was drawn. None is an offset of 0. + """ + offset = np.zeros(len(output_voxel_size)) if dataset_offset_nm is None else np.asarray(dataset_offset_nm, float) + return offset - np.asarray(output_voxel_size, float) / 2 + + +def whole_voxels(values, what: str) -> list: + """``values`` (a block size in voxels, as floats) as whole numbers. + + A ValueError names ``what`` when one is not whole: truncating a block of + 177.6 voxels to 177 would make a volume whose chunks are not the model's + blocks. + """ + out = [] + for value in map(float, values): + if not math.isfinite(value) or abs(value - round(value)) > WHOLE_VOXEL_TOLERANCE: + raise ValueError(f"{what} ({[float(v) for v in values]} voxels) is not whole voxels") + out.append(round(value)) + return out + + +def _field(model, name): + return model.get(name) if isinstance(model, Mapping) else getattr(model, name, None) + + +def _numbers(values, what: str, positive: bool = False) -> list: + """``values`` as a list of finite floats; a ValueError naming ``what`` otherwise.""" + try: + out = [float(v) for v in values] + except (TypeError, ValueError): + raise ValueError(f"{what} must be numbers, not {values!r}") from None + if not all(math.isfinite(v) and (v > 0 or not positive) for v in out): + raise ValueError(f"{what} must be {'positive' if positive else 'finite'} numbers, not {out}") + return out + + +def block_shapes(model) -> tuple: + """``(input_size, output_size)``, a model's blocks in voxels. + + ``model`` (a mapping, or an object with these attributes) has + ``input_voxel_size`` and ``output_voxel_size``, and either + ``input_shape``/``output_shape`` in voxels or ``read_shape``/ + ``write_shape`` in nm, which is what a server reports. + """ + if _field(model, "input_shape") is not None: + return (whole_voxels(_numbers(_field(model, "input_shape"), "input_shape", positive=True), "the model's input block"), + whole_voxels(_numbers(_field(model, "output_shape"), "output_shape", positive=True), "the model's output block")) + claimed_in = _numbers(_field(model, "input_voxel_size"), "input_voxel_size", positive=True) + claimed_out = _numbers(_field(model, "output_voxel_size"), "output_voxel_size", positive=True) + read = _numbers(_field(model, "read_shape"), "read_shape", positive=True) + write = _numbers(_field(model, "write_shape"), "write_shape", positive=True) + return (whole_voxels([r / v for r, v in zip(read, claimed_in)], "the model's input block"), + whole_voxels([w / v for w, v in zip(write, claimed_out)], "the model's output block")) + + +def plan_volume(level, model) -> dict: + """Where a new volume lies for ``model`` over the raw ``level`` it reads. + + ``level`` is that level as the model reads it, z, y, x: ``voxel`` its + voxel size in nm, ``corner`` its voxel-0 lower corner in nm (not the OME + translation, which is the centre), ``shape`` its voxels. On the server + it is the level ``select_level`` picks for the model's input voxel size, + at its own voxel size, or the raw resampled to the model's input voxel + size when Resample is on; in the browser, the level ``chooseRawLevel`` + picks. ``model`` is as ``block_shapes`` takes it. + + Answers the plan's fields of ``VolumeGeometry``, as JSON-ready lists: + ``input_voxel_size`` the level's own, ``output_voxel_size`` the level's + times the model's ratio of output to input voxel size (where both + servers serve the prediction), the model's own as the claimed sizes, + ``chunk_size`` the model's output block and ``input_size`` its input + block, ``dataset_offset_nm`` voxel 0's centre (the level's corner plus + half an output voxel) and ``dataset_shape_voxels`` the level's extent in + output voxels, rounded up, then padded to whole chunks. + + The ratio is the model's: snapping the output voxel size to a raw level + of its own could break it (on levels of 10, 20 and 40 nm, a model reading + 8 nm and writing 16 read the 10 nm level and wrote a 10 nm volume, while + its server serves the prediction at 20), and the volume's chunks were + then not the model's blocks. + """ + voxel = _numbers(_field(level, "voxel"), "the level's voxel size", positive=True) + corner = _numbers(_field(level, "corner"), "the level's corner") + shape = whole_voxels(_numbers(_field(level, "shape"), "the level's shape"), "the level's shape") + # The model's own, as plain numbers (ints kept ints): the root attrs are + # written from them as they are. + claimed_in = np.asarray(_field(model, "input_voxel_size")).tolist() + claimed_out = np.asarray(_field(model, "output_voxel_size")).tolist() + model_in = _numbers(claimed_in, "the model's input_voxel_size", positive=True) + model_out = _numbers(claimed_out, "the model's output_voxel_size", positive=True) + if not len(voxel) == len(corner) == len(shape) == len(model_in) == len(model_out): + raise ValueError(f"the level ({len(voxel)} axes) and the model ({len(model_in)}) do not have the same axes") + input_size, output_size = block_shapes(model) + + # In this order, as the browser's served level computes it + # (infer/geometry.ts outputLevel), so the two agree to the last bit. + output_voxel = [v * (o / i) for v, o, i in zip(voxel, model_out, model_in)] + offset = [c + w / 2 for c, w in zip(corner, output_voxel)] + # The level's extent in output voxels, rounded up so a partial voxel at + # the far end is covered, after rounding off the float noise of nm. + covered = [math.ceil(round(n * v / w, 6)) for n, v, w in zip(shape, voxel, output_voxel)] + padded = [-(-n // c) * c for n, c in zip(covered, output_size)] + return { + "output_voxel_size": output_voxel, + "input_voxel_size": voxel, + "claimed_output_voxel_size": claimed_out, + "claimed_input_voxel_size": claimed_in, + "chunk_size": output_size, + "input_size": input_size, + "dataset_offset_nm": offset, + "dataset_shape_voxels": padded, + } diff --git a/cellmap_flow/portable/volume_calls.py b/cellmap_flow/portable/volume_calls.py new file mode 100644 index 00000000..72222682 --- /dev/null +++ b/cellmap_flow/portable/volume_calls.py @@ -0,0 +1,88 @@ +"""What the browser calls to plan an annotation volume and its good regions: +the functions ``web/src/finetune/volume.ts``, ``web/src/finetune/good-regions.ts`` +and ``web/src/finetune/run.ts`` run through chunkmirage's +``Engine.call("cellmap_flow.portable.volume_calls:", args, arrays)`` +(``calls.py`` says the shape). Each takes plain numbers in its JSON and +answers JSON; the rules are ``volume.py``'s and ``regions.py``'s, which the +server calls in its process. A refusal (a ValueError of those rules) is +answered as ``{"error": }``, so the page shows the sentence the +server's route would, not a Python traceback. +""" + +import functools + +from cellmap_flow.portable import regions, volume + + +def _refusal(fn): + """``fn`` answering ``{"error": }`` where it raises a ValueError.""" + + @functools.wraps(fn) + def call(args, arrays): + try: + return fn(args, arrays) + except ValueError as e: + return {"error": str(e)} + + return call + + +@_refusal +def plan_volume(args, arrays): + """``volume.plan_volume`` of ``level`` (``shape``, ``voxel``, ``corner``: + the raw level the model reads) and ``model`` (its voxel sizes and its + blocks, as ``block_shapes`` takes them), with ``dtype``, the labels' + (``volume.annotation_dtype`` of ``output_type``, what the model trains on).""" + plan = volume.plan_volume(args["level"], args["model"]) + return {**plan, "dtype": volume.annotation_dtype(args.get("output_type"))} + + +def _layer_boxes(entries) -> list: + """Each entry as the good_regions layer draws it: its box and label + (``good`` without a label), or None for one that is not a region.""" + out = [] + for entry in entries: + box = regions.region_box_nm(entry) + if box is None: + out.append(None) + continue + label = entry.get("label") + out.append({"offset_nm": box[0].tolist(), "shape_nm": box[1].tolist(), + "label": label if isinstance(label, str) else "good"}) + return out + + +@_refusal +def mark_region(args, arrays): + """The region a mark makes (``regions.new_region``): a box of the size + ``size_nm`` asks for (``regions.region_size_nm``, the volume's output + block by default: ``volume`` has its ``chunk_size`` and + ``output_voxel_size``, or is null) centred on ``centre_nm``, labelled + ``label`` or after the ``regions`` already stored. Answers the region, + and ``boxes``, the layer's boxes of the stored regions and it + (``region_boxes``).""" + grid = args.get("volume") or {} + size = regions.region_size_nm(args.get("size_nm"), grid.get("chunk_size"), grid.get("output_voxel_size")) + stored = list(args.get("regions") or []) + region = regions.new_region(args["centre_nm"], size, len(stored), args.get("label")) + return {"region": region, "boxes": _layer_boxes([*stored, region])} + + +def region_boxes(args, arrays): + """The good_regions layer's boxes of ``regions``: per entry its + ``offset_nm``, ``shape_nm`` and ``label``, or null for one that is not a + region (``regions.region_box_nm``), so the layer's ids stay the entries' + indices.""" + return _layer_boxes(args.get("regions") or []) + + +def anchor_boxes(args, arrays): + """``regions.anchor_boxes`` of ``regions`` on the grid of ``volume`` + (its ``dataset_offset_nm``, ``output_voxel_size``, ``dataset_shape_voxels`` + and ``chunk_size``, the model's output block): the boxes, and the + entries skipped, each by its index in ``regions``.""" + grid = args["volume"] + corner = volume.volume_corner_nm(grid.get("dataset_offset_nm"), grid["output_voxel_size"]) + return regions.anchor_boxes( + args.get("regions") or [], corner, grid["output_voxel_size"], grid["dataset_shape_voxels"], grid["chunk_size"] + ) diff --git a/cellmap_flow/post/segment.py b/cellmap_flow/post/segment.py index 6288e78a..e39ed6f1 100644 --- a/cellmap_flow/post/segment.py +++ b/cellmap_flow/post/segment.py @@ -28,87 +28,23 @@ The segmentation libraries are imported inside the functions: the postprocessors import this module, and every chain is read through them, in processes that never segment anything (``tests/utils/test_import_hygiene``). +``connected_components``, ``remove_small`` and the parsing of their options +live in ``cellmap_flow/portable/label_it.py``, which the browser runs too. """ import numpy as np -# The connectivities in the order the forms list them, with their names. -CONNECTIVITIES = {1: "faces", 2: "faces and edges", 3: "all neighbours"} - - -def as_connectivity(value, ndim=3) -> int: - """``value`` as scipy's connectivity for an ``ndim``-D array: 1 up to ``ndim``. - - Forms send strings, so "2" is 2. A value past ``ndim`` (3 for one 2D - slice) is ``ndim``: every neighbour, which is what it asked for. - Raises ValueError for anything that is not 1, 2 or 3. - """ - try: - connectivity = int(value) - except (TypeError, ValueError): - raise ValueError(f"connectivity must be 1, 2 or 3, got {value!r}") - if connectivity not in CONNECTIVITIES: - raise ValueError(f"connectivity must be 1, 2 or 3, got {value!r}") - return min(connectivity, ndim) - - -def as_bool(value) -> bool: - """A form's "true"/"false" as a bool; anything else by Python's truth.""" - if isinstance(value, str): - return value.strip().lower() in ("true", "1", "yes", "on") - return bool(value) - - -def _structure(ndim, connectivity): - from scipy.ndimage import generate_binary_structure - - return generate_binary_structure(ndim, as_connectivity(connectivity, ndim)) - - -def remove_small(labels, min_size=0): - """``labels`` with objects of fewer than ``min_size`` voxels made background. - - The rest are numbered 1..n again, in the order they had. ``labels`` - must already be numbered without large gaps (any segmenter's output): - the sizes are counted per id. - """ - labels = np.asarray(labels) - if min_size is None or int(min_size) <= 1 or not labels.any(): - return labels.astype(np.uint64, copy=False) - sizes = np.bincount(labels.ravel().astype(np.intp)) - keep = sizes >= int(min_size) - keep[0] = False - new_ids = np.zeros(sizes.size, dtype=np.uint64) - new_ids[keep] = np.arange(1, np.count_nonzero(keep) + 1, dtype=np.uint64) - return new_ids[labels.astype(np.intp)] - - -def connected_components(mask, connectivity=1, min_size=0, per_slice=False): - """An id per connected object of ``mask`` (its nonzero voxels). - - ``per_slice``: each z slice of a (z, y, x) mask is labelled on its own, - in 2D, its ids following the slice before's. For data whose objects are - 2D (what Cellpose sees), or annotations painted slice by slice: an - object touching itself across slices is still one per slice. - ``min_size``: objects of fewer voxels are background (specks a - threshold let through); per slice, each slice's piece is counted alone. - """ - from scipy.ndimage import label - - mask = np.asarray(mask) != 0 - if per_slice: - if mask.ndim != 3: - raise ValueError(f"per_slice labels (z, y, x) masks, got shape {mask.shape}") - structure = _structure(2, connectivity) - labels = np.zeros(mask.shape, dtype=np.uint64) - count = 0 - for z, plane in enumerate(mask): - plane_labels, n = label(plane, structure) - labels[z] = np.where(plane_labels > 0, plane_labels.astype(np.uint64) + np.uint64(count), 0) - count += n - else: - labels, _ = label(mask, _structure(mask.ndim, connectivity)) - return remove_small(labels, min_size) +# The connected components and their options are cellmap_flow.portable's, the +# Python the browser runs too (Label it's components method there), and are +# imported here for the postprocessors and the other segmenters. +from cellmap_flow.portable.label_it import ( # noqa: F401 + CONNECTIVITIES, + _structure, + as_bool, + as_connectivity, + connected_components, + remove_small, +) def mutex_watershed(affinities, offsets, bias=0.0, min_size=0): diff --git a/cellmap_flow/serving/probe.py b/cellmap_flow/serving/probe.py index 53ceedf1..20c09369 100644 --- a/cellmap_flow/serving/probe.py +++ b/cellmap_flow/serving/probe.py @@ -13,23 +13,14 @@ import logging -logger = logging.getLogger(__name__) +# The activation classes and their reading of a range are written once in +# cellmap_flow.portable, where the browser's Python (a seed's probability) +# reads them too: UNIT [0, 1], a sigmoid or softmax already applied; +# SIGNED_UNIT [-1, 1], a tanh; UNBOUNDED, no output activation (logits, +# distances, affinities...). +from cellmap_flow.portable.label_it import SIGNED_UNIT, UNBOUNDED, UNIT, classify_output_range # noqa: F401 -UNIT = "unit" # [0, 1] -- sigmoid/softmax already applied -SIGNED_UNIT = "signed_unit" # [-1, 1] -- tanh already applied -UNBOUNDED = "unbounded" # no output activation: logits, distances, affinities... - - -def classify_output_range(lo: float, hi: float) -> str: - """Bucket an observed (min, max) output range into an activation class.""" - # Small tolerance: a saturating activation can land a hair outside its - # asymptote in float32. - eps = 1e-4 - if lo >= -eps and hi <= 1.0 + eps: - return UNIT - if lo >= -1.0 - eps and hi <= 1.0 + eps: - return SIGNED_UNIT - return UNBOUNDED +logger = logging.getLogger(__name__) def looks_like_affinities(out_channels=None, model_name="", channels_names=None) -> bool: diff --git a/pixi.lock b/pixi.lock index 828e8901..d76fcba8 100644 --- a/pixi.lock +++ b/pixi.lock @@ -137,7 +137,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.9-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/06/cf/1731c0dd182ee35f80a39e63afb3d03056ea0dd561b7498932b10b2e34d9/filelock-4.0.7-py3-none-any.whl @@ -170,6 +169,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/78/afa8f85e1f3cb276202ad3af9e36765feee1f0c5ec2942d8522b321e5afc/torchvision-0.29.0-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/ae/9366619c9c8f6459cb4de77b0661cca113a0a78e4069f47002473e594da6/edt-3.1.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -384,7 +384,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/ea/2e31b67051e91a133189e9c000c222502ddc6969856416de0d095de4c0b0/typer-0.27.3-py3-none-any.whl @@ -405,6 +404,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -623,7 +623,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.9-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: https://files.pythonhosted.org/packages/01/4f/83454fafd628e1e7e1726d74e44fb2332be5969d04c6182ca1fecb6c580e/filelock-4.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/38/311ac322f46ecd789f1342bb7adb0a14b940cf54b5db0e92d4f449aa6cf8/google_auth-2.59.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -649,6 +648,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/ae/9366619c9c8f6459cb4de77b0661cca113a0a78e4069f47002473e594da6/edt-3.1.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -851,7 +851,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/ea/2e31b67051e91a133189e9c000c222502ddc6969856416de0d095de4c0b0/typer-0.27.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl @@ -868,6 +867,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -1085,7 +1085,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.9-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -1121,6 +1120,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/78/afa8f85e1f3cb276202ad3af9e36765feee1f0c5ec2942d8522b321e5afc/torchvision-0.29.0-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/ae/9366619c9c8f6459cb4de77b0661cca113a0a78e4069f47002473e594da6/edt-3.1.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -1341,7 +1341,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -1367,6 +1366,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -1634,7 +1634,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl @@ -1662,6 +1661,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/e4/8c526e16c80c52fbd982e2a048bea10dcd256dc7422b29bb8d6cf76bf3b7/fastremap-1.20.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -1882,7 +1882,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/98/04e766a6de99e6f7f955ecb7829e8d5a557de3427cb85be2236de54dda0c/uvloop-0.23.0-cp312-cp312-macosx_10_13_universal2.whl @@ -1902,6 +1901,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/14/f3f0ba083d2094392d5eabf56db5ea6ba469fd6e927afd187042054ea68a/regex-2026.9.29-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl @@ -2168,7 +2168,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/4e/0703722c46447fa03c9425386b3ef3e90254a7c1eb5da654c3c33b210317/pdbpp-0.12.1-py3-none-any.whl @@ -2198,6 +2197,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/e4/8c526e16c80c52fbd982e2a048bea10dcd256dc7422b29bb8d6cf76bf3b7/fastremap-1.20.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -2449,7 +2449,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/4e/0703722c46447fa03c9425386b3ef3e90254a7c1eb5da654c3c33b210317/pdbpp-0.12.1-py3-none-any.whl @@ -2471,6 +2470,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/14/f3f0ba083d2094392d5eabf56db5ea6ba469fd6e927afd187042054ea68a/regex-2026.9.29-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl @@ -2661,7 +2661,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.9-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2696,6 +2695,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/34/78/afa8f85e1f3cb276202ad3af9e36765feee1f0c5ec2942d8522b321e5afc/torchvision-0.29.0-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/ae/9366619c9c8f6459cb4de77b0661cca113a0a78e4069f47002473e594da6/edt-3.1.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl @@ -2905,7 +2905,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl @@ -2929,6 +2928,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl @@ -3155,7 +3155,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.9-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: git+https://github.com/funkelab/funlib.learn.torch.git#228e15f41d755ee5c7e50cfaa26f7ed5110b55e4 - pypi: git+https://github.com/mzouink/fly-organelles?rev=ab89c10#ab89c10470ab819e0ec232d75f0fb117ad3ba9c2 @@ -3191,6 +3190,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/ae/9366619c9c8f6459cb4de77b0661cca113a0a78e4069f47002473e594da6/edt-3.1.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -3410,7 +3410,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: git+https://github.com/funkelab/funlib.learn.torch.git#228e15f41d755ee5c7e50cfaa26f7ed5110b55e4 - pypi: git+https://github.com/mzouink/fly-organelles?rev=ab89c10#ab89c10470ab819e0ec232d75f0fb117ad3ba9c2 @@ -3437,6 +3436,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -3654,7 +3654,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.1.9-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/ea/2e31b67051e91a133189e9c000c222502ddc6969856416de0d095de4c0b0/typer-0.27.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl @@ -3678,6 +3677,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/ae/9366619c9c8f6459cb4de77b0661cca113a0a78e4069f47002473e594da6/edt-3.1.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -3879,7 +3879,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/ea/2e31b67051e91a133189e9c000c222502ddc6969856416de0d095de4c0b0/typer-0.27.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl @@ -3898,6 +3897,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -4155,7 +4155,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-2.18.4-pyhd8ed1ab_0.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl @@ -4183,6 +4182,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/e4/8c526e16c80c52fbd982e2a048bea10dcd256dc7422b29bb8d6cf76bf3b7/fastremap-1.20.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl @@ -4413,7 +4413,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-h31dac16_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda - pypi: ./ - - pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/98/04e766a6de99e6f7f955ecb7829e8d5a557de3427cb85be2236de54dda0c/uvloop-0.23.0-cp312-cp312-macosx_10_13_universal2.whl @@ -4433,6 +4432,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/37/14/f3f0ba083d2094392d5eabf56db5ea6ba469fd6e927afd187042054ea68a/regex-2026.9.29-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl @@ -9937,7 +9937,7 @@ packages: - requests - scipy - click - - chunkmirage @ git+https://github.com/JaneliaSciComp/chunkmirage@7307e36b499f099bb0d5fd80f88cb013c0230387 + - chunkmirage>=0.1.0a2,<0.2 - dacapo-ml ; extra == 'dacapo' - cellpose==3.1.1.1 ; extra == 'cellpose' - bioimageio-core[onnx,pytorch]==0.11.0 ; extra == 'bioimageio' @@ -9961,36 +9961,6 @@ packages: - tensorboard ; extra == 'finetune' - google-genai>=1.0 ; extra == 'ai-annotate' requires_python: '>=3.11,<4.0' -- pypi: git+https://github.com/JaneliaSciComp/chunkmirage?rev=7307e36b499f099bb0d5fd80f88cb013c0230387#7307e36b499f099bb0d5fd80f88cb013c0230387 - name: chunkmirage - version: 0.1.0a1 - requires_dist: - - numpy>=1.26 - - numcodecs>=0.12 - - tensorstore>=0.1.60 - - starlette>=0.37 - - uvicorn[standard]>=0.29 - - hypercorn[h2]>=0.17 - - pydantic>=2.6 - - typer>=0.12 - - threadpoolctl>=3.1 - - h5py>=3.10 ; extra == 'hdf5' - - tifffile>=2024.1 ; extra == 'tiff' - - imagecodecs>=2024.1 ; extra == 'tiff' - - scipy>=1.12 ; extra == 'ops' - - scikit-image>=0.22 ; extra == 'mesh' - - dracopy>=1.4 ; extra == 'mesh' - - mcp>=1.0 ; extra == 'mcp' - - neuroglancer>=2.40 ; extra == 'viewer' - - cryptography>=42 ; extra == 'https' - - torch>=2.6 ; extra == 'gpu' - - scipy>=1.12 ; extra == 'gpu' - - torch>=2.6 ; extra == 'cpu' - - scipy>=1.12 ; extra == 'cpu' - - cupy-cuda12x>=13 ; extra == 'cupy' - - scipy>=1.12 ; extra == 'cupy' - - chunkmirage[hdf5,tiff,ops,mesh,mcp,viewer,https] ; extra == 'all' - requires_python: '>=3.11' - pypi: git+https://github.com/briossant/neuroglancer.git?rev=fae54db8c667176a602a34f0d9d1028956009836#fae54db8c667176a602a34f0d9d1028956009836 name: neuroglancer version: 0.0.0.post1.dev2083 @@ -11364,6 +11334,46 @@ packages: requires_dist: - numpy requires_python: '>=3.8,<4' +- pypi: https://files.pythonhosted.org/packages/35/40/b200c53ec39f73b80ce2f532e5a6e3089958944f55f765e981f0bf552f76/chunkmirage-0.1.0a2-py3-none-any.whl + name: chunkmirage + version: 0.1.0a2 + sha256: 36371ea4e142c25050fab04fb14f5b35e252b5208021f12e5b33d43e892a1391 + requires_dist: + - hypercorn[h2]>=0.17 + - numcodecs>=0.12 + - numpy>=1.26 + - pydantic>=2.6 + - starlette>=0.37 + - tensorstore>=0.1.60 + - threadpoolctl>=3.1 + - typer>=0.12 + - uvicorn[standard]>=0.29 + - worldarray>=0.1,<1 + - cryptography>=42 ; extra == 'all' + - dracopy>=1.4 ; extra == 'all' + - h5py>=3.10 ; extra == 'all' + - imagecodecs>=2024.1 ; extra == 'all' + - mcp>=1.0 ; extra == 'all' + - neuroglancer>=2.40 ; extra == 'all' + - scikit-image>=0.22 ; extra == 'all' + - scipy>=1.12 ; extra == 'all' + - tifffile>=2024.1 ; extra == 'all' + - scipy>=1.12 ; extra == 'cpu' + - torch>=2.6 ; extra == 'cpu' + - cupy-cuda12x>=13 ; extra == 'cupy' + - scipy>=1.12 ; extra == 'cupy' + - scipy>=1.12 ; extra == 'gpu' + - torch>=2.6 ; extra == 'gpu' + - h5py>=3.10 ; extra == 'hdf5' + - cryptography>=42 ; extra == 'https' + - mcp>=1.0 ; extra == 'mcp' + - dracopy>=1.4 ; extra == 'mesh' + - scikit-image>=0.22 ; extra == 'mesh' + - scipy>=1.12 ; extra == 'ops' + - imagecodecs>=2024.1 ; extra == 'tiff' + - tifffile>=2024.1 ; extra == 'tiff' + - neuroglancer>=2.40 ; extra == 'viewer' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl name: sphinxcontrib-devhelp version: 2.0.0 diff --git a/pyproject.toml b/pyproject.toml index 395f6a42..657de837 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,12 +42,16 @@ dependencies = [ "requests", "scipy", "click", - # Serves the predictions (serving/chunkmirage_server.py): its main since - # PR #17 merged (channel ops, chunkmirage.serve, the cache size given, - # queued chunks in the order they were asked for). Not on PyPI yet: a - # release of cellmap-flow needs one, as PyPI refuses a dependency given - # by URL. - "chunkmirage @ git+https://github.com/JaneliaSciComp/chunkmirage@7307e36b499f099bb0d5fd80f88cb013c0230387", + # Serves the predictions (serving/chunkmirage_server.py). From PyPI by + # version, not from git by commit: PyPI refuses an upload whose + # dependencies name a URL, so a git pin would keep cellmap-flow from + # being released. 0.1.0a2 was built from chunkmirage's commit 584b343; + # the browser engine's pin in web/chunkmirage.lock is that commit or a + # later one. Naming a pre-release in the lower bound lets pip take the + # alpha without --pre; the upper bound is there because the plugin API + # (the ops and the consumer hook this app registers) may still change + # before 0.1.0. + "chunkmirage>=0.1.0a2,<0.2", ] # extras diff --git a/tests/ai_annotate/test_geometry.py b/tests/ai_annotate/test_geometry.py index 231c4741..3d358d40 100644 --- a/tests/ai_annotate/test_geometry.py +++ b/tests/ai_annotate/test_geometry.py @@ -1,34 +1,13 @@ -"""Planning the plane an AI annotation reads and the voxels it writes. +"""Which plane the viewer shows, and the server's refusal of a point outside the volume. -The volumes here have their voxel 0's corner at the world origin: an 8 nm -volume's ``dataset_offset_nm`` is 4 (voxel 0's centre), so annotation voxel -i covers [8i, 8i + 8) nm and the arithmetic in the expectations is plain. +The plane's maths (``plan_plane``, ``raw_box``, the screen layout) is +``cellmap_flow.portable.ai_annotate``'s, tested in tests/portable/test_ai_annotate.py. """ -import numpy as np import pytest from cellmap_flow.ai_annotate.errors import AIAnnotateError -from cellmap_flow.ai_annotate.geometry import ( - depth_axis_from_layout, - plan_plane, - raw_box, -) - - -def _volume(output=(8, 8, 8), input=(4, 4, 4)): - output = np.asarray(output, dtype=float) - return { - "output_voxel_size": list(output), - "input_voxel_size": list(input), - "dataset_offset_nm": list(output / 2), # voxel 0's centre: its corner is at 0 - } - - -SHAPE = (100, 200, 300) - - -# --- which plane the user is looking at --------------------------------------------- +from cellmap_flow.ai_annotate.geometry import depth_axis_from_layout, plan_plane @pytest.mark.parametrize( @@ -51,110 +30,9 @@ def test_depth_axis_from_layout_with_zyx_dimensions(layout, depth): assert depth_axis_from_layout(layout) == depth -# How the image is laid out as the viewer's panel draws the plane (to_screen, -# from_screen) is web/test/fixtures/ai_annotate.json's "screen" cases, which -# tests/web/test_fixtures_fresh.py regenerates from this code and the vitest -# parity suite checks the TypeScript against. - - -# --- the plane -------------------------------------------------------------------- - - -@pytest.mark.parametrize("depth_axis", [0, 1, 2]) -def test_a_plane_is_centred_on_the_point_one_voxel_thick(depth_axis): - # The point is in annotation voxel (50, 100, 150), off its centre. - point = (50 * 8 + 1, 100 * 8 + 3, 150 * 8 + 6) - plan = plan_plane(_volume(), SHAPE, point, depth_axis, crop_size_px=64) - - lo, hi = np.array(plan.write_lo), np.array(plan.write_hi) - voxel = np.array([50, 100, 150]) - for axis in range(3): - if axis == depth_axis: - assert (lo[axis], hi[axis]) == (voxel[axis], voxel[axis] + 1) - else: - # 64 voxels whose centres lie within 256 nm of the point. - assert hi[axis] - lo[axis] == 64 - assert np.all(np.abs((np.arange(lo[axis], hi[axis]) + 0.5) * 8 - point[axis]) <= 256) - assert plan.write_shape == (64, 64) - - # The raw is the same area at 4 nm, one input voxel thick at the point. - raw_lo, raw_size = np.array(plan.raw_offset_nm), np.array(plan.raw_shape_nm) - for axis in plan.plane_axes: - assert raw_lo[axis] == lo[axis] * 8 - assert raw_size[axis] == 64 * 8 - assert raw_size[depth_axis] == 4 - assert raw_lo[depth_axis] + 2 == point[depth_axis] - assert plan.image_shape == (128, 128) - assert plan.resolution_nm == 4 - assert plan.input_voxel_size == (4.0, 4.0, 4.0) - assert plan.plane_name == ("XY", "XZ", "YZ")[depth_axis] - # The point's pixel: 4 nm pixels from the read's corner, laid out as the - # panel draws the plane (rows, cols): y, x with x leftwards; x, z; y, z. - down, across = {0: (1, 2), 1: (2, 0), 2: (1, 0)}[depth_axis] - row, col = (int((point[a] - raw_lo[a]) // 4) for a in (down, across)) - assert plan.click_px == (row, 128 - 1 - col if depth_axis == 0 else col) - - -def test_a_point_on_a_voxel_boundary_takes_the_voxel_above(): - plan = plan_plane(_volume(), SHAPE, (80, 800, 1200), 0, crop_size_px=8) - assert (plan.write_lo[0], plan.write_hi[0]) == (10, 11) - - -def test_the_volume_offset_is_voxel_0s_centre(): - # dataset_offset_nm 100: voxel 0 covers [96, 104), so 103 nm is in voxel 0 - # and 104 nm in voxel 1, on every axis. - volume = {**_volume(), "dataset_offset_nm": [100.0] * 3} - plan = plan_plane(volume, SHAPE, (103, 104 + 8 * 20, 104 + 8 * 30), 0, crop_size_px=4) - assert plan.write_lo == (0, 20 - 1, 30 - 1) - assert plan.write_hi == (1, 20 + 3, 30 + 3) - assert plan.raw_offset_nm[1:] == (96 + 19 * 8, 96 + 29 * 8) - - -def test_the_write_box_is_clipped_at_the_volume_edge(): - # Voxel (50, 3, 297): 3 voxels from y's start, 2 from x's end. - point = (50 * 8 + 4, 3 * 8 + 4, 297 * 8 + 4) - plan = plan_plane(_volume(), SHAPE, point, 0, crop_size_px=64) - assert plan.write_lo == (50, 0, 297 - 32) - assert plan.write_hi == (51, 3 + 32, 300) - assert plan.write_shape == (35, 35) - # The raw read covers exactly the clipped box, so the image does too. - assert plan.raw_offset_nm[1:] == (0.0, (297 - 32) * 8) - assert plan.raw_shape_nm[1:] == (35 * 8, 35 * 8) - assert plan.image_shape == (70, 70) - # x runs leftwards in the XY plane, as neuroglancer draws it. - assert plan.click_px == ((3 * 8 + 4) // 4, 70 - 1 - (32 * 8 + 4) // 4) - - -@pytest.mark.parametrize( - "point", - [ - (-1, 800, 1200), # before the volume along the depth axis - (400, 800, 300 * 8), # just past the far end in x - (400, -0.5, 1200), - (100 * 8 + 4, 800, 1200), - ], -) -def test_a_point_outside_the_volume_is_refused(point): +def test_a_point_outside_the_volume_is_refused_for_the_user(): + volume = {"output_voxel_size": [8.0] * 3, "input_voxel_size": [4.0] * 3, "dataset_offset_nm": [4.0] * 3} with pytest.raises(AIAnnotateError) as caught: - plan_plane(_volume(), SHAPE, point, 0, crop_size_px=64) - assert caught.value.category == "refused" + plan_plane(volume, (100, 200, 300), (-1, 800, 1200), 0, crop_size_px=64) + assert caught.value.category == "refused" and caught.value.http_status == 400 assert "outside the annotation volume" in caught.value.user_message - - -def test_the_raw_box_reads_the_plane_on_the_raws_grid(): - plan = plan_plane(_volume(), SHAPE, (50 * 8 + 1, 800, 1200), 0, crop_size_px=64) - # A raw at 4 nm from the origin: exactly the box's voxels, and the one - # input voxel the point is in (401 nm is in [400, 404)). - box = raw_box(plan, (4, 4, 4), (0, 0, 0)) - assert box.begin == (100, (800 - 256) // 4, (1200 - 256) // 4) - assert box.shape == (1, 128, 128) - # A raw whose corner is off the box's grid: each pixel reads the raw - # voxel its centre is in (the first pixel's centre is 546 nm in y). - for corner, begin in (((-1,) * 3, (100, 136, 236)), ((1,) * 3, (100, 136, 236)), ((3,) * 3, (99, 135, 235))): - box = raw_box(plan, (4, 4, 4), corner) - assert box.begin == begin, corner - assert box.shape == (1, 128, 128) - # A coarser raw: 8 nm voxels, 64 of them; along z the one holding 401 nm. - box = raw_box(plan, (8, 8, 8), (0, 0, 0)) - assert box.begin == (50, 68, 118) - assert box.shape == (1, 64, 64) diff --git a/tests/ai_annotate/test_mask_extraction.py b/tests/ai_annotate/test_mask_extraction.py deleted file mode 100644 index e87e6216..00000000 --- a/tests/ai_annotate/test_mask_extraction.py +++ /dev/null @@ -1,23 +0,0 @@ -"""The model's painted reply to a bool mask.""" - -import numpy as np -import pytest -from PIL import Image - -from cellmap_flow.ai_annotate.mask_extraction import extract_mask -from cellmap_flow.ai_annotate.organelles import ORGANELLES - - -def _reply(colours): - """A one-row image of the given RGB colours.""" - return Image.fromarray(np.array([colours], dtype=np.uint8)) - - -@pytest.mark.parametrize("key", list(ORGANELLES)) -def test_every_catalog_colour_can_be_found_and_neighbours_are_not(key): - target = ORGANELLES[key].rgb - near = tuple(int(np.clip(c + (-20 if c > 128 else 15), 0, 255)) for c in target) - others = [p.rgb for p in ORGANELLES.values() if p.rgb != target] - mask = extract_mask(_reply([target, near, (0, 0, 0), (255, 255, 255), *others]), target) - assert mask[0, :2].all(), f"{key}: its own colour was not found" - assert not mask[0, 2:].any(), f"{key}: another colour counted as {target}" diff --git a/tests/ai_annotate/test_pipeline.py b/tests/ai_annotate/test_pipeline.py index 4d21d8b8..f0269d8a 100644 --- a/tests/ai_annotate/test_pipeline.py +++ b/tests/ai_annotate/test_pipeline.py @@ -3,15 +3,14 @@ The round trips are the point: a raw with a bright block at a known world position, a plane planned through it, read, masked by brightness, mapped to the write box and labelled must label exactly the annotation voxels that -cover the block, on every plane and whatever the voxel sizes are. +cover the block, on every plane and whatever the voxel sizes are. The mask's +objects and ids on their own are tests/portable/test_ai_annotate.py's. """ -from types import SimpleNamespace - import numpy as np import pytest -from cellmap_flow.ai_annotate.geometry import from_screen, plan_plane, to_screen +from cellmap_flow.ai_annotate.geometry import plan_plane from cellmap_flow.ai_annotate.pipeline import ( labels_for_box, mask_to_write_shape, @@ -162,61 +161,3 @@ def bright_below_the_point_along(axis): side = bright_below_the_point_along(across) near, far = (side[:, 16:], side[:, :16]) if depth_axis == 0 else (side[:, :16], side[:, 16:]) assert near.all() and not far.any() - - -# --- the mask on the box ---------------------------------------------------------- - - -def test_a_one_pixel_line_parts_two_objects_on_coarser_voxels(): - # 4 nm pixels on 8 nm voxels: each voxel takes every second pixel, and - # the line the model left between the two objects is on one none takes. - plan = plan_plane(_volume(None, (8, 8, 8), (4, 4, 4)), (24, 32, 40), (81, 113, 177), 0, crop_size_px=16) - assert plan.image_shape == (32, 32) and plan.write_shape == (16, 16) - mask = np.ones(plan.image_shape, dtype=bool) - mask[:, 16] = False - labels = labels_for_box(mask_to_write_shape(mask, plan), np.zeros((1, 16, 16), dtype=np.uint8), 0, count_up=False) - assert set(np.unique(labels)) == {2, 3} - - -def _objects(mask, depth_axis): - """``mask`` (the box's plane, z, y, x order) cut into objects by - ``mask_to_write_shape``, from an image of the box's own size.""" - plan = SimpleNamespace(write_shape=mask.shape, depth_axis=depth_axis, display_dimensions=None, - from_screen=lambda image: from_screen(image, depth_axis)) - return mask_to_write_shape(to_screen(mask, depth_axis), plan) - - -@pytest.mark.parametrize("depth_axis", [0, 1, 2]) -def test_labels_for_box_gives_each_object_an_id(depth_axis): - mask = np.zeros((6, 8), dtype=bool) - mask[0:2, 0:2] = True - mask[3:5, 4:7] = True - mask[5, 7] = True # touches the second only at a corner: an object of its own - existing = np.zeros(np.insert(mask.shape, depth_axis, 1), dtype=np.uint8) - labels = np.squeeze(labels_for_box(_objects(mask, depth_axis), existing, depth_axis, count_up=False), axis=depth_axis) - assert labels.dtype == np.uint8 - assert np.all(labels[~mask] == 1) - ids = {int(labels[0, 0]), int(labels[3, 4]), int(labels[5, 7])} - assert ids == {2, 3, 4} - assert len(np.unique(labels[0:2, 0:2])) == 1 and len(np.unique(labels[3:5, 4:7])) == 1 - - -def test_labels_for_box_keeps_a_painted_objects_id(): - mask = np.zeros((1, 6, 6), dtype=bool)[0] - mask[1:4, 1:4] = True - existing = np.zeros((1, 6, 6), dtype=np.uint8) - existing[0, 1, 1] = 7 # the user painted part of the object as 7 - existing[0, 5, 5] = 9 # and another object elsewhere - labels = labels_for_box(mask, existing, 0, count_up=False)[0] - assert np.all(labels[1:4, 1:4] == 7) - - -def test_labels_for_box_counts_up_on_an_instance_volume(): - mask = np.zeros((4, 8), dtype=bool) - mask[:, 0:2] = True - mask[:, 4:6] = True - existing = np.zeros((4, 1, 8), dtype=np.uint16) - existing[0, 0, 7] = 300 - labels = labels_for_box(_objects(mask, 1), existing, 1, count_up=True)[:, 0, :] - assert labels.dtype == np.uint16 - assert {int(labels[0, 0]), int(labels[0, 4])} == {301, 302} diff --git a/tests/ai_annotate/test_routes.py b/tests/ai_annotate/test_routes.py index b1d7b973..71c7d271 100644 --- a/tests/ai_annotate/test_routes.py +++ b/tests/ai_annotate/test_routes.py @@ -446,7 +446,7 @@ def test_running_out_of_ids_says_what_to_do(dashboard, ai_config, ai_volume, syn def no_room(*args, **kwargs): raise ValueError("3 new objects do not fit in the volume's uint8 labels: make a new annotation volume") - monkeypatch.setattr(routes.pipeline, "labels_for_box", no_room) + monkeypatch.setattr(routes.pipeline, "accept_labels", no_room) response = dashboard.post(f"{API}/accept", json={"annotate_id": status["annotate_id"], "overwrite": False}) assert response.status_code == 409 and "make a new annotation volume" in response.get_json()["error"] diff --git a/tests/finetune/test_target_transforms.py b/tests/finetune/test_target_transforms.py index b9b40c4c..956add88 100644 --- a/tests/finetune/test_target_transforms.py +++ b/tests/finetune/test_target_transforms.py @@ -1,14 +1,80 @@ """Target transforms: an annotation (0 unannotated, 1 background, 2+ objects) as -the target and the mask of voxels a loss is computed on. The binary and -affinity targets are checked against the browser's by web/test/parity/targets.""" +the target and the mask of voxels a loss is computed on. The maths is +cellmap_flow/portable/targets.py, which the browser runs too, through +portable/targets_calls.py; the server's classes wrap it.""" +import json import math import numpy as np import pytest import torch -from cellmap_flow.finetune.target_transforms import DistanceTargetTransform, IntervalTargetTransform +from cellmap_flow.finetune.target_transforms import ( + AffinityTargetTransform, + BroadcastBinaryTargetTransform, + DistanceTargetTransform, + IntervalTargetTransform, +) +from cellmap_flow.portable import targets + + +def test_the_binary_target_is_the_objects_masked_where_painted_on_every_channel(): + ann = torch.tensor([0, 1, 2, 3], dtype=torch.float32).reshape(1, 1, 1, 1, 4) + target, mask = BroadcastBinaryTargetTransform(2)(ann) + assert target.shape == mask.shape == (1, 2, 1, 1, 4) and target.dtype == torch.float32 + assert target[0, :, 0, 0].tolist() == [[0, 0, 1, 1]] * 2 + assert mask[0, :, 0, 0].tolist() == [[0, 1, 1, 1]] * 2 + + +def test_an_affinity_is_one_within_an_object_and_counts_where_both_voxels_are_painted(): + """Along x: 2|2 the same object, 2|3 two objects, 3|1 object and background, + 1|0 a pair half unpainted, and the last voxel's neighbour past the patch. + A channel past the offsets (an LSD, say) and one whose offset is None + train nothing.""" + ann = torch.tensor([2, 2, 3, 1, 0], dtype=torch.float32).reshape(1, 1, 1, 1, 5) + target, mask = AffinityTargetTransform([[0, 0, 1], None], num_channels=3)(ann) + assert target[0, 0, 0, 0].tolist() == [1, 0, 0, 0, 0] + assert mask[0, 0, 0, 0].tolist() == [1, 1, 1, 0, 0] + assert not mask[0, 1:].any() and not target[0, 1:].any() + + +@pytest.mark.parametrize("dz", [3, 4, 7]) +def test_an_offset_no_shorter_than_the_patch_pairs_nothing(dz): + """A one-slice patch's z channel (a 2D student's head) has an empty mask, + and so has a long-range offset past a short patch, either way along the + axis. Between one and two patch lengths that used to raise, as slicing + counted the stop from the far end.""" + ann = np.full((3, 4, 4), 2, np.uint8) + _, one_slice = targets.affinity(ann[:1], [[1, 0, 0], [0, 1, 0]]) + assert not one_slice[0].any() and one_slice[1].any() + target, mask = targets.affinity(ann, [[dz, 0, 0], [-dz, 0, 0], [2, 0, 0]]) + assert not mask[:2].any() and not target[:2].any() + assert mask[2, :1].all() and not mask[2, 1:].any() + + +def test_the_browser_call_answers_a_chunks_targets_as_the_server_makes_them(): + """targets_calls.targets marshalled as chunkmirage's worker marshals it: an + instance volume's uint16 ids in, float32 (channels, z, y, x) back.""" + from chunkmirage import calls + + rng = np.random.default_rng(0) + labels = rng.choice(np.array([0, 1, 2, 300, 40000], np.uint16), size=(2, 3, 4)) + offsets = [[1, 0, 0], [0, 1, 0], [0, 0, 1], None] + args = {"shape": [2, 3, 4], "labels_dtype": "uint16", "channels": 5} + cases = [ + ({"offsets": None}, BroadcastBinaryTargetTransform(5)), + ({"offsets": offsets}, AffinityTargetTransform(offsets, 5)), + # The browser knows only its manifest's channel names; the server's + # rule (label_it.affinity_offsets) reads the first affinity channels + # as z, y, x and leaves the rest (LSDs) out of the loss. + ({"channel_names": ["m_aff_z", "m_aff_y", "m_aff_x", "lsd0", "lsd1"]}, AffinityTargetTransform(offsets[:3], 5)), + ] + for given, server in cases: + value, (target, mask) = calls.call("cellmap_flow.portable.targets_calls:targets", json.dumps({**args, **given}), [labels.tobytes()]) + want = [t[0].numpy() for t in server(torch.from_numpy(labels.astype(np.float32))[None, None])] + assert json.loads(value) == {"channels": 5} + assert [np.frombuffer(b, np.float32).reshape(5, 2, 3, 4).tolist() for b in (target, mask)] == [w.tolist() for w in want] def _soft(d, sigma): diff --git a/tests/portable/test_ai_annotate.py b/tests/portable/test_ai_annotate.py new file mode 100644 index 00000000..fbba6abb --- /dev/null +++ b/tests/portable/test_ai_annotate.py @@ -0,0 +1,244 @@ +"""AI-assisted annotation's maths (cellmap_flow.portable.ai_annotate), which +both modes run: the plane planned around a point, the mask in a model's +reply, and the labels Accept writes. + +The volumes here have their voxel 0's corner at the world origin: an 8 nm +volume's ``dataset_offset_nm`` is 4 (voxel 0's centre), so annotation voxel +i covers [8i, 8i + 8) nm and the arithmetic in the expectations is plain. +The round trips through a raw on disk are tests/ai_annotate/test_pipeline.py's. +""" + +from types import SimpleNamespace + +import numpy as np +import pytest + +from cellmap_flow.ai_annotate.organelles import ORGANELLES +from cellmap_flow.portable.ai_annotate import ( + Refused, + accept_labels, + extract_mask, + from_screen, + labels_for_box, + mask_to_write_shape, + overlay, + plan_plane, + raw_box, + to_screen, +) + + +def _volume(output=(8, 8, 8), input=(4, 4, 4)): + output = np.asarray(output, dtype=float) + return { + "output_voxel_size": list(output), + "input_voxel_size": list(input), + "dataset_offset_nm": list(output / 2), # voxel 0's centre: its corner is at 0 + } + + +SHAPE = (100, 200, 300) + + +# --- the plane -------------------------------------------------------------------- + + +@pytest.mark.parametrize("depth_axis", [0, 1, 2]) +def test_a_plane_is_centred_on_the_point_one_voxel_thick(depth_axis): + # The point is in annotation voxel (50, 100, 150), off its centre. + point = (50 * 8 + 1, 100 * 8 + 3, 150 * 8 + 6) + plan = plan_plane(_volume(), SHAPE, point, depth_axis, crop_size_px=64) + + lo, hi = np.array(plan.write_lo), np.array(plan.write_hi) + voxel = np.array([50, 100, 150]) + for axis in range(3): + if axis == depth_axis: + assert (lo[axis], hi[axis]) == (voxel[axis], voxel[axis] + 1) + else: + # 64 voxels whose centres lie within 256 nm of the point. + assert hi[axis] - lo[axis] == 64 + assert np.all(np.abs((np.arange(lo[axis], hi[axis]) + 0.5) * 8 - point[axis]) <= 256) + assert plan.write_shape == (64, 64) + + # The raw is the same area at 4 nm, one input voxel thick at the point. + raw_lo, raw_size = np.array(plan.raw_offset_nm), np.array(plan.raw_shape_nm) + for axis in plan.plane_axes: + assert raw_lo[axis] == lo[axis] * 8 + assert raw_size[axis] == 64 * 8 + assert raw_size[depth_axis] == 4 + assert raw_lo[depth_axis] + 2 == point[depth_axis] + assert plan.image_shape == (128, 128) + assert plan.resolution_nm == 4 + assert plan.input_voxel_size == (4.0, 4.0, 4.0) + assert plan.plane_name == ("XY", "XZ", "YZ")[depth_axis] + # The point's pixel: 4 nm pixels from the read's corner, laid out as the + # panel draws the plane (rows, cols): y, x with x leftwards; x, z; y, z. + down, across = {0: (1, 2), 1: (2, 0), 2: (1, 0)}[depth_axis] + row, col = (int((point[a] - raw_lo[a]) // 4) for a in (down, across)) + assert plan.click_px == (row, 128 - 1 - col if depth_axis == 0 else col) + + +def test_a_point_on_a_voxel_boundary_takes_the_voxel_above(): + plan = plan_plane(_volume(), SHAPE, (80, 800, 1200), 0, crop_size_px=8) + assert (plan.write_lo[0], plan.write_hi[0]) == (10, 11) + + +def test_the_volume_offset_is_voxel_0s_centre(): + # dataset_offset_nm 100: voxel 0 covers [96, 104), so 103 nm is in voxel 0 + # and 104 nm in voxel 1, on every axis. + volume = {**_volume(), "dataset_offset_nm": [100.0] * 3} + plan = plan_plane(volume, SHAPE, (103, 104 + 8 * 20, 104 + 8 * 30), 0, crop_size_px=4) + assert plan.write_lo == (0, 20 - 1, 30 - 1) + assert plan.write_hi == (1, 20 + 3, 30 + 3) + assert plan.raw_offset_nm[1:] == (96 + 19 * 8, 96 + 29 * 8) + + +def test_the_write_box_is_clipped_at_the_volume_edge(): + # Voxel (50, 3, 297): 3 voxels from y's start, 2 from x's end. + point = (50 * 8 + 4, 3 * 8 + 4, 297 * 8 + 4) + plan = plan_plane(_volume(), SHAPE, point, 0, crop_size_px=64) + assert plan.write_lo == (50, 0, 297 - 32) + assert plan.write_hi == (51, 3 + 32, 300) + assert plan.write_shape == (35, 35) + # The raw read covers exactly the clipped box, so the image does too. + assert plan.raw_offset_nm[1:] == (0.0, (297 - 32) * 8) + assert plan.raw_shape_nm[1:] == (35 * 8, 35 * 8) + assert plan.image_shape == (70, 70) + # x runs leftwards in the XY plane, as neuroglancer draws it. + assert plan.click_px == ((3 * 8 + 4) // 4, 70 - 1 - (32 * 8 + 4) // 4) + + +@pytest.mark.parametrize( + "point", + [ + (-1, 800, 1200), # before the volume along the depth axis + (400, 800, 300 * 8), # just past the far end in x + (400, -0.5, 1200), + (100 * 8 + 4, 800, 1200), + ], +) +def test_a_point_outside_the_volume_is_refused(point): + with pytest.raises(Refused, match="outside the annotation volume"): + plan_plane(_volume(), SHAPE, point, 0, crop_size_px=64) + + +def test_the_raw_box_reads_the_plane_on_the_raws_grid(): + plan = plan_plane(_volume(), SHAPE, (50 * 8 + 1, 800, 1200), 0, crop_size_px=64) + # A raw at 4 nm from the origin: exactly the box's voxels, and the one + # input voxel the point is in (401 nm is in [400, 404)). + begin, count = raw_box(plan, (4, 4, 4), (0, 0, 0)) + assert begin == (100, (800 - 256) // 4, (1200 - 256) // 4) + assert count == (1, 128, 128) + # A raw whose corner is off the box's grid: each pixel reads the raw + # voxel its centre is in (the first pixel's centre is 546 nm in y). + for corner, first in (((-1,) * 3, (100, 136, 236)), ((1,) * 3, (100, 136, 236)), ((3,) * 3, (99, 135, 235))): + assert raw_box(plan, (4, 4, 4), corner) == (first, (1, 128, 128)), corner + # A coarser raw: 8 nm voxels, 64 of them; along z the one holding 401 nm. + assert raw_box(plan, (8, 8, 8), (0, 0, 0)) == ((50, 68, 118), (1, 64, 64)) + + +# --- the mask in the model's reply -------------------------------------------------- + + +@pytest.mark.parametrize("key", list(ORGANELLES)) +def test_every_catalog_colour_can_be_found_and_neighbours_are_not(key): + target = ORGANELLES[key].rgb + near = tuple(int(np.clip(c + (-20 if c > 128 else 15), 0, 255)) for c in target) + others = [p.rgb for p in ORGANELLES.values() if p.rgb != target] + mask = extract_mask(np.array([[target, near, (0, 0, 0), (255, 255, 255), *others]], dtype=np.uint8), target) + assert mask[0, :2].all(), f"{key}: its own colour was not found" + assert not mask[0, 2:].any(), f"{key}: another colour counted as {target}" + + +def test_the_overlay_tints_what_accept_writes_halfway_to_the_colour(): + # A 2x2 mask on a 4x4 image: each mask pixel covers 2x2 image pixels. + # Halfway from grey 2 to 255 is 128.5, which rounds to the even 128. + image = np.full((4, 4, 4), 2, dtype=np.uint8) + tinted = overlay(image, np.array([[1, 0], [0, 0]]), (255, 0, 0)) + assert tinted.shape == (4, 4, 3) + assert tinted[:2, :2].tolist() == [[[128, 1, 1]] * 2] * 2 + assert (tinted[2:] == 2).all() and (tinted[:2, 2:] == 2).all() + + +# --- the mask on the box ------------------------------------------------------------ + + +def test_a_one_pixel_line_parts_two_objects_on_coarser_voxels(): + # 4 nm pixels on 8 nm voxels: each voxel takes every second pixel, and + # the line the model left between the two objects is on one none takes. + plan = plan_plane(_volume((8, 8, 8), (4, 4, 4)), (24, 32, 40), (81, 113, 177), 0, crop_size_px=16) + assert plan.image_shape == (32, 32) and plan.write_shape == (16, 16) + mask = np.ones(plan.image_shape, dtype=bool) + mask[:, 16] = False + labels = labels_for_box(mask_to_write_shape(mask, plan), np.zeros((1, 16, 16), dtype=np.uint8), 0, count_up=False) + assert set(np.unique(labels)) == {2, 3} + + +def _objects(mask, depth_axis): + """``mask`` (the box's plane, z, y, x order) cut into objects by + ``mask_to_write_shape``, from an image of the box's own size.""" + plan = SimpleNamespace(write_shape=mask.shape, depth_axis=depth_axis, display_dimensions=None, + from_screen=lambda image: from_screen(image, depth_axis)) + return mask_to_write_shape(to_screen(mask, depth_axis), plan) + + +@pytest.mark.parametrize("depth_axis", [0, 1, 2]) +def test_labels_for_box_gives_each_object_an_id(depth_axis): + mask = np.zeros((6, 8), dtype=bool) + mask[0:2, 0:2] = True + mask[3:5, 4:7] = True + mask[5, 7] = True # touches the second only at a corner: an object of its own + existing = np.zeros(np.insert(mask.shape, depth_axis, 1), dtype=np.uint8) + labels = np.squeeze(labels_for_box(_objects(mask, depth_axis), existing, depth_axis, count_up=False), axis=depth_axis) + assert labels.dtype == np.uint8 + assert np.all(labels[~mask] == 1) + ids = {int(labels[0, 0]), int(labels[3, 4]), int(labels[5, 7])} + assert ids == {2, 3, 4} + assert len(np.unique(labels[0:2, 0:2])) == 1 and len(np.unique(labels[3:5, 4:7])) == 1 + + +def test_labels_for_box_keeps_a_painted_objects_id(): + mask = np.zeros((1, 6, 6), dtype=bool)[0] + mask[1:4, 1:4] = True + existing = np.zeros((1, 6, 6), dtype=np.uint8) + existing[0, 1, 1] = 7 # the user painted part of the object as 7 + existing[0, 5, 5] = 9 # and another object elsewhere + labels = labels_for_box(mask, existing, 0, count_up=False)[0] + assert np.all(labels[1:4, 1:4] == 7) + + +def test_labels_for_box_counts_up_on_an_instance_volume(): + mask = np.zeros((4, 8), dtype=bool) + mask[:, 0:2] = True + mask[:, 4:6] = True + existing = np.zeros((4, 1, 8), dtype=np.uint16) + existing[0, 0, 7] = 300 + labels = labels_for_box(_objects(mask, 1), existing, 1, count_up=True)[:, 0, :] + assert labels.dtype == np.uint16 + assert {int(labels[0, 0]), int(labels[0, 4])} == {301, 302} + + +@pytest.mark.parametrize("dtype, instance_target, ids", [ + (np.uint8, True, {2, 3}), # a uint8 volume reuses free ids, whatever the model + (np.uint16, False, {2, 3}), # a binary target only sees foreground + (np.uint16, True, {301, 302}), # an instance target on wider ids counts up from the box's largest + (np.uint32, True, {301, 302}), +]) +def test_accept_counts_ids_up_only_for_an_instance_target_on_a_wide_volume(dtype, instance_target, ids): + # A one-voxel-thick box along z, 1 x 8 in plane, with an id painted at its end. + objects = np.zeros((1, 8), dtype=np.int32) + objects[0, :2], objects[0, 4:6] = 1, 2 + existing = np.zeros((1, 1, 8), dtype=dtype) + existing[0, 0, 7] = 300 if np.dtype(dtype).itemsize > 1 else 30 + labels = accept_labels(objects, existing, 0, instance_target, overwrite=False)[0, 0] + assert {int(labels[0]), int(labels[4])} == ids + + +def test_overwrite_lets_objects_replace_paint_but_keeps_objects_the_model_missed(): + objects = np.array([[1, 1, 0, 0]], dtype=np.int32) + existing = np.array([[[5, 0, 6, 0]]], dtype=np.uint8) + labels = accept_labels(objects, existing, 0, False, overwrite=True)[0, 0] + # The model's object keeps the id painted on it; its background fills + # only the unpainted voxel (0 leaves a voxel as it is), so object 6, + # which the model missed, stays. + assert labels.tolist() == [5, 5, 0, 1] diff --git a/tests/portable/test_fill.py b/tests/portable/test_fill.py new file mode 100644 index 00000000..863c4795 --- /dev/null +++ b/tests/portable/test_fill.py @@ -0,0 +1,64 @@ +"""The box decisions both modes make (cellmap_flow.portable.fill): which voxels +a write changes, the ids new objects get, a split's ids, and what an undo puts +back. The writes to MinIO around them are tests/ai_annotate/test_paint_box.py's +and tests/utils/test_view_labels.py's.""" + +import numpy as np +import pytest + +from cellmap_flow.portable.fill import fresh_ids, paint, relabel_objects, restore, rewrite + + +def test_a_seed_fills_only_unpainted_voxels_and_overwrite_replaces_painted_ones(): + existing = np.array([0, 0, 1, 5, 0], dtype=np.uint8) + labels = np.array([2, 1, 2, 2, 0], dtype=np.uint8) # 0: leave as it is + after, counts = paint(existing, labels) + assert after.tolist() == [2, 1, 1, 5, 0] and counts == (1, 1, 0) + after, counts = paint(existing, labels, overwrite=True) + assert after.tolist() == [2, 1, 2, 2, 0] and counts == (3, 1, 2) + assert paint(existing, np.zeros_like(existing)) == (None, (0, 0, 0)) + + +def test_new_ids_are_the_lowest_free_or_count_up_and_say_when_none_fit(): + existing = np.array([2, 4, 9], dtype=np.uint8) + assert fresh_ids(existing, 3, count_up=False).tolist() == [3, 5, 6] + assert fresh_ids(existing.astype(np.uint16), 2, count_up=True).tolist() == [10, 11] + with pytest.raises(ValueError, match="do not fit in the volume's uint8 labels"): + fresh_ids(np.arange(2, 256, dtype=np.uint8), 1, count_up=False) + + +def test_a_wall_through_an_object_splits_it_and_a_stroke_across_two_merges_them(): + # One object (3) cut by a background wall (1) in every slice; and objects + # 5 and 6 joined by a stroke of 6, 5 the bigger. + box = np.zeros((2, 3, 9), dtype=np.uint8) + box[:, :, 0:2] = 3 + box[:, :, 2] = 1 + box[:, :, 3:5] = 3 + box[:, 0, 6:9] = 5 + box[:, 1, 6] = 6 + labels, counts = relabel_objects(box) + assert counts == {"objects": 3, "split": 1, "merged": 1} + assert len({int(labels[0, 0, 0]), int(labels[0, 0, 3])}) == 2 # the two halves of 3 apart + assert (labels[:, :2, 6:9][box[:, :2, 6:9] > 0] == 5).all() # 6 joins 5, the bigger + assert (labels[box < 2] == box[box < 2]).all() # background and unpainted alone + after, changed = rewrite(box, labels) + assert changed == int(np.count_nonzero(labels != box)) and (after == labels).all() + + +def test_a_wall_in_one_slice_splits_only_per_slice_and_then_each_slice_is_its_own_object(): + box = np.full((3, 3, 7), 5, dtype=np.uint8) + box[1, :, 3] = 1 # a wall in the middle slice: the sides still touch above and below it + _, counts = relabel_objects(box) + assert counts == {"objects": 1, "split": 0, "merged": 0} + labels, counts = relabel_objects(box, per_slice=True) + assert counts == {"objects": 4, "split": 3, "merged": 0} + assert len({int(labels[0, 0, 0]), int(labels[1, 0, 0]), int(labels[1, 0, 6]), int(labels[2, 0, 0])}) == 4 + + +def test_undo_puts_back_only_what_nobody_touched_since(): + before = np.array([0, 0, 0, 7], dtype=np.uint8) + after = np.array([2, 2, 1, 7], dtype=np.uint8) + current = np.array([2, 9, 1, 7], dtype=np.uint8) # voxel 1 painted again since + restored, n = restore(current, before, after) + assert restored.tolist() == [0, 9, 0, 7] and n == 2 + assert restore(before, before, after) == (None, 0) diff --git a/tests/portable/test_label_it.py b/tests/portable/test_label_it.py new file mode 100644 index 00000000..0256a338 --- /dev/null +++ b/tests/portable/test_label_it.py @@ -0,0 +1,159 @@ +"""Label it's segmentation of a prediction (``cellmap_flow.portable.label_it``), +the maths both modes run, and the browser's entry points to it +(``label_it_calls``), called here as chunkmirage's worker calls them: JSON +arguments, the arrays as bytes. The routes around it are +``tests/utils/test_view_labels.py``'s and the browser's +``web/test/unit/backend-view-labels.test.ts``.""" + +import json +import subprocess +import sys +import warnings + +import numpy as np +import pytest + +from cellmap_flow.portable import label_it, label_it_calls +from cellmap_flow.portable.label_it import SIGNED_UNIT, UNBOUNDED, UNIT + + +def test_importing_it_loads_no_scipy(): + """The browser's Python starts with numpy alone: scipy loads on the first seed, not when a dataset opens.""" + code = ( + "import sys\n" + "import cellmap_flow.portable.label_it, cellmap_flow.portable.label_it_calls\n" + "print('scipy' in sys.modules)\n" + ) + out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True) + assert out.stdout.strip() == "False" + + +@pytest.mark.parametrize("output_class, at_boundary", [ + (UNIT, 0.5), (SIGNED_UNIT, 0.0), (UNBOUNDED, 0.0), (None, 0.0), # None: read off the values, logits here +]) +def test_the_probability_is_one_half_at_each_activations_boundary(output_class, at_boundary): + prediction = np.array([[[[at_boundary, -200.0 if output_class in (UNBOUNDED, None) else at_boundary]]]], np.float32) + with warnings.catch_warnings(): + warnings.simplefilter("error") # a logit far below the boundary is 0, not an overflow warning + p = label_it.probability(prediction, output_class) + assert p.dtype == np.float32 and p[0, 0, 0, 0] == 0.5 and p[0, 0, 0, 1] in (0.0, 0.5) + + +def test_channels_past_the_models_are_cut_and_one_wholly_past_them_refused(): + prediction = np.zeros((2, 1, 1, 1), np.float32) + assert label_it.probability(prediction, UNIT, slice(0, 3)).shape[0] == 2 + with pytest.raises(ValueError): + label_it.probability(prediction, UNIT, slice(3, 4)) + + +def test_seed_options_take_the_routes_defaults_and_refuse_what_they_refuse(): + assert label_it.seed_options({}) == { + "method": "components", "threshold": 0.5, "min_size": 0, "connectivity": 1, "per_slice": False, + } + assert label_it.seed_options({"threshold": "0.3", "connectivity": "2", "per_slice": "true", "min_size": None}) == { + "method": "components", "threshold": 0.3, "min_size": 0, "connectivity": 2, "per_slice": True, + } + # connectivity null is given, so refused, as the server has always read it. + for body in ({"threshold": 1.5}, {"min_size": -1}, {"method": "watershed"}, {"connectivity": 4}, {"connectivity": None}): + with pytest.raises(ValueError): + label_it.seed_options(body) + + +@pytest.mark.parametrize("names, offsets", [ + (["mito_aff_z", "mito_aff_y", "mito_aff_x"], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + # Counted, whatever the names say after _aff: the server's rule. + (["mito_aff_x", "mito_aff_y", "mito_aff_z", "lsd_0"], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + (["a_aff", "b_aff"], [[1, 0, 0], [0, 1, 0]]), + ([f"aff_long_{i}_aff" for i in range(9)], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]), + (["nuc"], None), +]) +def test_an_affinity_models_offsets_by_its_channel_names(names, offsets): + assert label_it.affinity_offsets(names) == offsets + + +def test_a_seed_reads_the_selected_channel_else_the_affinities_else_every_channel(): + offsets = [[1, 0, 0], [0, 1, 0]] + assert label_it.seed_channels("affinities", offsets, "3") == slice(3, 4) + assert label_it.seed_channels("affinities", offsets) == slice(0, 2) + assert label_it.seed_channels("binary", None) is None + with pytest.raises(ValueError): + label_it.seed_channels("binary", None, -1) + + +def test_components_are_strictly_over_the_threshold_and_numbered_per_slice_on_from_the_last(): + p = np.zeros((1, 2, 3, 5), np.float32) + p[0, :, 1, 0] = 0.5 # at the threshold: background + p[0, :, 1, 2:4] = 0.9 # a 2-voxel object in each slice + p[0, 1, 0, 0] = 0.9 # a speck in the second slice + objects = label_it.components(p, UNIT, None, 0.5, per_slice=True) + # In raster order within a slice: the second slice's speck comes before its object. + assert objects[0, 1, 2] == 1 and objects[1, 0, 0] == 2 and objects[1, 1, 2] == 3 and objects[:, 1, 0].max() == 0 + # Without per_slice the slices' objects touch; min_size drops the speck. + assert label_it.components(p, UNIT, None, 0.5, min_size=2).max() == 1 + + +# volume voxel 8 nm from voxel 0's centre at 4 nm (its corner at 0); the +# served level's voxel, translation and shape; the box; the served index of +# each of its voxels along every axis, and the box shrunk. +@pytest.mark.parametrize("served_voxel, translation, shape, lo, hi, index, box", [ + pytest.param(8, 4, 12, 2, 9, lambda i: i, (2, 9), id="the-same-grid"), + pytest.param(16, 8, 8, 1, 9, lambda i: i // 2, (1, 9), id="coarser-two-voxels-to-one"), + pytest.param(4, 2, 40, 0, 6, lambda i: 2 * i + 1, (0, 6), id="finer-the-one-each-centre-is-in"), + pytest.param(8, 24, 10, 0, 6, lambda i: i - 2, (2, 6), id="served-from-20nm-the-box-shrinks"), + pytest.param(8, 4, 6, 3, 9, lambda i: i, (3, 6), id="past-its-end-the-box-shrinks"), +]) +def test_each_annotation_voxel_reads_the_served_voxel_its_centre_is_in(served_voxel, translation, shape, lo, hi, index, box): + indices, new_lo, new_hi = label_it.placement([lo] * 3, [hi] * 3, [8] * 3, [4] * 3, [translation] * 3, [served_voxel] * 3, [shape] * 3) + assert (new_lo, new_hi) == ([box[0]] * 3, [box[1]] * 3) + for axis in indices: + assert axis.tolist() == [index(i) for i in range(*box)] + + +def test_a_layer_drawn_at_another_voxel_size_keeps_its_offset_in_voxels_and_none_is_outside(): + # Declared at 8 nm from 4 nm, drawn at 16: its corner stays at voxel 0, its voxels twice as wide. + indices, _, _ = label_it.placement([0] * 3, [4] * 3, [8] * 3, [4] * 3, [4] * 3, [8] * 3, [10] * 3, drawn=[16] * 3) + assert indices[0].tolist() == [0, 0, 1, 1] + assert label_it.placement([5, 0, 0], [8, 4, 4], [8] * 3, [4] * 3, [4] * 3, [8] * 3, [4] * 3) is None + + +def _call(name, args, arrays=()): + """``label_it_calls.`` as the worker calls it: args through JSON, arrays as bytes.""" + return getattr(label_it_calls, name)(json.loads(json.dumps(args)), [bytes(a) for a in arrays]) + + +def test_the_browsers_seed_is_the_servers_on_the_annotation_grid(): + """A model served at twice the volume's voxel: placed, read, gathered and + segmented by the calls as the server's seed does it (label_it.components).""" + rng = np.random.default_rng(0) + served = rng.normal(size=(3, 8, 8, 8)).astype(np.float32) + box = {"lo": [1, 2, 3], "hi": [11, 12, 13], "volume": {"output_voxel_size": [8] * 3, "dataset_offset_nm": [4] * 3}, + "level": {"origin": [8] * 3, "voxel": [16] * 3, "shape": [8] * 3}} + plan = _call("seed_plan", {"channel_names": ["m_aff_z", "m_aff_y", "m_aff_x"], "select_channel": None, **box}) + assert plan["channels"] == [0, 3] and plan["offsets"] == [[1, 0, 0], [0, 1, 0], [0, 0, 1]] + placement = plan["placement"] + region = served[(slice(None),) + tuple(slice(a, b) for a, b in zip(placement["read_lo"], placement["read_hi"]))] + options = _call("seed_options", {"min_size": 2})["options"] + args = {**options, "output_class": None, "channels": plan["channels"], "offsets": plan["offsets"], + "indices": placement["indices"], "shape": list(region.shape)} + + answer, (objects,) = _call("seed", args, [np.ascontiguousarray(region)]) + + prediction = label_it.gather(region, [np.asarray(i) for i in placement["indices"]]) + expected = label_it.components(prediction, None, slice(0, 3), 0.5, 2) + objects = np.frombuffer(objects, np.uint32).reshape(expected.shape) + np.testing.assert_array_equal(objects, expected) + assert answer["count"] == expected.max() > 1 and expected.shape == (10, 10, 10) + + # The mutex watershed's input, for the browser's own watershed. + answer, (affinities,) = _call("seed", {**args, "method": "mutex_watershed"}, [np.ascontiguousarray(region)]) + np.testing.assert_array_equal(np.frombuffer(affinities, np.float32).reshape(answer["shape"]), + label_it.affinities(prediction, None, plan["offsets"])) + + +def test_the_calls_answer_a_refusal_where_the_server_answers_400(): + assert "error" in _call("seed_options", {"connectivity": None}) + assert "error" in _call("seed_plan", {"channel_names": ["nuc"], "select_channel": -1, "lo": [0] * 3, "hi": [1] * 3, + "volume": {"output_voxel_size": [8] * 3}, "level": {"origin": [4] * 3, "voxel": [8] * 3, "shape": [1] * 3}}) + region = np.zeros((1, 1, 1, 1), np.float32) + args = {**label_it.seed_options({}), "output_class": None, "channels": [3, 4], "offsets": None, "indices": [[0]] * 3, "shape": [1, 1, 1, 1]} + assert "error" in _call("seed", args, [region]) diff --git a/tests/portable/test_volume.py b/tests/portable/test_volume.py new file mode 100644 index 00000000..92d56718 --- /dev/null +++ b/tests/portable/test_volume.py @@ -0,0 +1,80 @@ +"""Planning an annotation volume and its good regions on numbers alone +(cellmap_flow.portable.volume and regions), the rules the server runs in its +process and the browser through volume_calls.""" + +import pytest + +from cellmap_flow.portable import regions, volume, volume_calls + + +def _model(voxel_in, voxel_out, block_in, block_out): + return {"input_voxel_size": [voxel_in] * 3, "output_voxel_size": [voxel_out] * 3, + "input_shape": [block_in] * 3, "output_shape": [block_out] * 3} + + +@pytest.mark.parametrize("level, model, voxel, offset, shape", [ + # The level at the model's size: on its grid, from its corner (-4 nm, a + # Janelia pyramid's), each output voxel two of the level's, the 33rd + # level voxel's half an output voxel rounded up, then whole chunks of 4. + pytest.param({"voxel": [8.0] * 3, "corner": [-4.0] * 3, "shape": [33, 32, 32]}, _model(8, 16, 12, 4), + [16.0] * 3, [4.0] * 3, [20, 16, 16], id="the model's level"), + # No 8 nm level: the model reads the 10 nm one, and its prediction is + # served at 20 nm, where the volume lies too (snapping 16 nm to a level + # of its own made it 10 nm, chunks half the model's blocks). + pytest.param({"voxel": [10.0] * 3, "corner": [0.0] * 3, "shape": [40] * 3}, _model(8, 16, 12, 4), + [20.0] * 3, [10.0] * 3, [20] * 3, id="the model's ratio on another level"), +]) +def test_a_volume_lies_on_the_grid_its_prediction_is_served_on(level, model, voxel, offset, shape): + plan = volume.plan_volume(level, model) + assert (plan["output_voxel_size"], plan["dataset_offset_nm"], plan["dataset_shape_voxels"]) == (voxel, offset, shape) + assert (plan["input_voxel_size"], plan["chunk_size"], plan["input_size"]) == (level["voxel"], [4] * 3, [12] * 3) + assert plan["claimed_output_voxel_size"] == [16] * 3, "the model's own, ints kept ints" + + +def test_a_block_in_nm_is_whole_voxels_or_refused(): + reported = {"input_voxel_size": [16] * 3, "output_voxel_size": [16] * 3, + "read_shape": [2848.0] * 3, "write_shape": [896.0] * 3} + assert volume.block_shapes(reported) == ([178] * 3, [56] * 3) + with pytest.raises(ValueError, match="not whole voxels"): + volume.block_shapes({**reported, "write_shape": [900.0] * 3}) + + +@pytest.mark.parametrize("size_nm, expected", [ + pytest.param(None, [56 * 16.0] * 3, id="none: one output patch"), + pytest.param(0, [56 * 16.0] * 3, id="0: one output patch, not a refusal"), + pytest.param(40, [40.0] * 3, id="one number"), + pytest.param([8, 48, 48], [8.0, 48.0, 48.0], id="three"), +]) +def test_the_size_a_mark_asks_for(size_nm, expected): + assert regions.region_size_nm(size_nm, [56] * 3, [16] * 3) == expected + assert regions.region_size_nm(None) == [regions.DEFAULT_REGION_SIZE_NM] * 3, "no volume" + + +@pytest.mark.parametrize("size_nm", [-1, [0, 8, 8], [8, 8], float("nan"), "40", True]) +def test_a_size_that_is_not_one_is_refused(size_nm): + with pytest.raises(ValueError, match="size_nm must be positive"): + regions.region_size_nm(size_nm) + + +def test_a_region_anchors_one_output_block_snapped_to_whole_voxels_and_clipped(): + grid = {"corner_nm": [0.0] * 3, "voxel_nm": [8.0] * 3, "shape_voxels": [10, 100, 100], "block": [4, 6, 6]} + marked = [ + regions.new_region([20.0, 404.0, 400.0], [32.0, 48.0, 48.0], 0), # centre (2.5, 50.5, 50) voxels + regions.new_region([4.0, 4.0, 4.0], [8.0] * 3, 1), # centre (0.5, 0.5, 0.5): its block runs past 0 + {"offset_nm": [0, 0], "shape_nm": [8, 8, 8]}, # not a region + regions.new_region([2000.0] * 3, [8.0] * 3, 2), # marked on another volume + ] + assert [r["label"] for r in marked if "label" in r] == ["good-1", "good-2", "good-3"] + placed = regions.anchor_boxes(marked, **grid) + assert placed["boxes"] == [{"region": 0, "lo": [1, 48, 47], "hi": [5, 54, 53]}, + {"region": 1, "lo": [0, 0, 0], "hi": [3, 4, 4]}] + assert (placed["malformed"], placed["outside"]) == ([2], [3]) + + +def test_the_browsers_calls_answer_a_refusal_as_its_sentence(): + level = {"voxel": [8.0] * 3, "corner": [-4.0] * 3, "shape": [32] * 3} + planned = volume_calls.plan_volume({"level": level, "model": _model(8, 8, 12, 4), "output_type": "affinities"}, []) + assert planned["dtype"] == "uint16" and planned["dataset_shape_voxels"] == [32] * 3 + assert volume_calls.plan_volume({"level": level, "model": _model(8, 8, 12, 4)}, [])["dtype"] == "uint8" + refused = volume_calls.mark_region({"centre_nm": [0, 0, 0], "size_nm": -1, "regions": []}, []) + assert refused == {"error": "size_nm must be positive"} diff --git a/tests/utils/test_cellmap_model_type.py b/tests/utils/test_cellmap_model_type.py index 47272984..52c21683 100644 --- a/tests/utils/test_cellmap_model_type.py +++ b/tests/utils/test_cellmap_model_type.py @@ -1,8 +1,10 @@ """The cellmap model type reads at the shape its TorchScript export runs at. -cellmap_models traces the export at the metadata's inference shape, and a -traced UNet runs only there: jrc_mus-livers_16nm_to_8nm_mito fails at its -training 216³ and runs at its inference 288³. +cellmap_models traces the export at whichever shape the exporter passed, and +the metadata records both the training and the inference shapes without +saying which: jrc_mus-livers_16nm_to_8nm_mito runs only at its inference 288³ +(it fails at 216³), mito_aff_unet_setup_16 only at its training 178³ (fed +378³ it still answers 56³). The type asks the model. """ from types import SimpleNamespace @@ -12,8 +14,23 @@ from cellmap_flow.models.configs.cellmap import CellMapModelConfig -class _Shrink(torch.nn.Module): - """A stand-in network: its output is the first ``out`` voxels along each axis.""" +class _TracedAt(torch.nn.Module): + """A stand-in for a traced export: fed ``inp`` voxels it answers ``out`` + voxels; fed anything else it fails, as a traced UNet does.""" + + def __init__(self, inp, out): + super().__init__() + self.inp, self.out = inp, out + + def forward(self, x): + if tuple(x.shape[-3:]) != (self.inp,) * 3: + raise RuntimeError("Kernel size can't be greater than actual input size") + return x[..., : self.out, : self.out, : self.out] + + +class _Fixed(torch.nn.Module): + """A stand-in for an export whose crops are baked in: ``out`` voxels + whatever the input, as setup_16 answers 56³ to a 378³ input.""" def __init__(self, out): super().__init__() @@ -35,22 +52,24 @@ def _metadata(**shapes): return SimpleNamespace(**fields) +INFERENCE = {"inference_input_shape": [288] * 3, "inference_output_shape": [212] * 3} + + @pytest.mark.parametrize( - "shapes, voxels_in, voxels_out", + "model, shapes, voxels_in, voxels_out", [ - ({"inference_input_shape": [288] * 3, "inference_output_shape": [212] * 3}, 288, 212), - ({}, 216, 68), + (_TracedAt(288, 212), INFERENCE, 288, 212), + (_Fixed(68), INFERENCE, 216, 68), + (_Fixed(68), {}, 216, 68), ], - ids=["inference-shapes", "training-shapes-only"], + ids=["traced-at-inference", "traced-at-training", "training-shapes-only"], ) -def test_reads_at_the_exports_inference_shape(monkeypatch, shapes, voxels_in, voxels_out): +def test_reads_at_the_shape_the_export_runs_at(monkeypatch, model, shapes, voxels_in, voxels_out): cellmap_model = pytest.importorskip("cellmap_models.model_export.cellmap_model") monkeypatch.setattr( cellmap_model, "CellmapModel", - lambda folder_path: SimpleNamespace( - folder_path=folder_path, metadata=_metadata(**shapes), ts_model=_Shrink(voxels_out) - ), + lambda folder_path: SimpleNamespace(folder_path=folder_path, metadata=_metadata(**shapes), ts_model=model), ) model_config = CellMapModelConfig(folder_path="/models/liver_mito") diff --git a/tests/web/test_fixtures_fresh.py b/tests/web/test_fixtures_fresh.py index 5389b19c..f2c42e10 100644 --- a/tests/web/test_fixtures_fresh.py +++ b/tests/web/test_fixtures_fresh.py @@ -60,41 +60,14 @@ def test_segment_fixtures_are_fresh(): ) -AI_SCRIPT = ROOT / "web" / "test" / "fixtures" / "make_ai_fixtures.py" - - -def test_ai_annotate_fixture_is_fresh(): - # The same for the AI-assisted annotation fixture (ai_annotate.json), - # which make_ai_fixtures.py writes from cellmap_flow.ai_annotate - # (geometry, pipeline, mask_extraction, prompts, organelles) and - # finetune/session/fill.py. - pythonpath = os.pathsep.join(p for p in (str(ROOT), os.environ.get("PYTHONPATH", "")) if p) - result = subprocess.run( - [sys.executable, str(AI_SCRIPT), "--check"], - cwd=ROOT, - env={**os.environ, "PYTHONPATH": pythonpath}, - capture_output=True, - text=True, - timeout=600, - ) - assert result.returncode == 0, ( - "web/test/fixtures/ai_annotate.json is stale: cellmap_flow.ai_annotate's geometry, pipeline, " - "mask_extraction, prompts or organelles, or finetune/session/fill.py, changed since it was generated. Regenerate with\n" - " python web/test/fixtures/make_ai_fixtures.py\n" - "(the pixi default environment's python) and commit the result with the change.\n\n" - f"{result.stdout}{result.stderr}" - ) - - LABEL_IT_SCRIPT = ROOT / "web" / "test" / "fixtures" / "make_label_it_fixtures.py" def test_label_it_fixture_is_fresh(): # The same for the Label it tools' fixture (label_it.json), which # make_label_it_fixtures.py writes from dashboard/routes/finetune/ - # view_labels.py (probability, segment_prediction, read_prediction), - # post/segment.py under them, and finetune/session/fill.py - # (relabel_objects, rewrite_foreground, restore_box). + # view_labels.py's segment_prediction (the mutex watershed), post/segment.py + # under it, and portable/label_it.py's affinities. pythonpath = os.pathsep.join(p for p in (str(ROOT), os.environ.get("PYTHONPATH", "")) if p) result = subprocess.run( [sys.executable, str(LABEL_IT_SCRIPT), "--check"], @@ -105,8 +78,8 @@ def test_label_it_fixture_is_fresh(): timeout=600, ) assert result.returncode == 0, ( - "web/test/fixtures/label_it.json is stale: view_labels.py's probability, segment_prediction or " - "read_prediction, post/segment.py, or finetune/session/fill.py changed since it was generated. Regenerate with\n" + "web/test/fixtures/label_it.json is stale: view_labels.py's segment_prediction, post/segment.py or " + "portable/label_it.py changed since it was generated. Regenerate with\n" " python web/test/fixtures/make_label_it_fixtures.py\n" "(the pixi default environment's python) and commit the result with the change.\n\n" f"{result.stdout}{result.stderr}" diff --git a/tests/web/test_pins.py b/tests/web/test_pins.py index cbf62f99..dc7f1b3d 100644 --- a/tests/web/test_pins.py +++ b/tests/web/test_pins.py @@ -1,21 +1,33 @@ -"""The two chunkmirage pins move together: pyproject.toml's, the commit the -Python inference server is built on, and web/chunkmirage.lock's, the commit -the browser engine is cloned from. One repository, so one commit, or the -browser serves a chunk format or protocol the server does not.""" +"""chunkmirage is pinned twice, and the two pins move together. +pyproject.toml's is a version range, the release the Python inference server +is installed from: PyPI refuses an upload whose dependencies name a URL, so +a git pin there would keep cellmap-flow from being released. +web/chunkmirage.lock's is a commit, the one the browser engine is cloned +from (the wheel carries no web/). One repository, so the lock's commit is +the release's own commit or a later one, or the browser serves a chunk +format or protocol the server does not. That order is checked by hand on a +bump (pyproject.toml's comment names the release's commit, for +``git merge-base --is-ancestor `` in a full clone): +web/chunkmirage/ is a shallow clone, with no history to check it in.""" import re +import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parents[2] -PYPROJECT_PIN = re.compile(r"chunkmirage @ git\+https://github\.com/JaneliaSciComp/chunkmirage@([0-9a-f]{40})") +# A requirement on chunkmirage by version: its name, any extras, then +# comparisons only (no "@ "). +BY_VERSION = re.compile(r"chunkmirage\s*(\[[^\]]*\])?\s*[<>=!~][^@;]*") LOCK_PIN = re.compile(r"^sha=([0-9a-f]{40})$", re.MULTILINE) -def _pin(path, pattern): - found = pattern.findall(path.read_text()) - assert len(found) == 1, f"{path.relative_to(ROOT)} should pin chunkmirage once, found {found}" - return found[0] - - -def test_the_python_and_browser_chunkmirage_pins_agree(): - assert _pin(ROOT / "pyproject.toml", PYPROJECT_PIN) == _pin(ROOT / "web" / "chunkmirage.lock", LOCK_PIN) +def test_the_server_takes_a_chunkmirage_release_and_the_browser_a_commit(): + dependencies = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["dependencies"] + chunkmirage = [d for d in dependencies if re.match(r"chunkmirage\b", d)] + assert len(chunkmirage) == 1, f"pyproject.toml should depend on chunkmirage once, found {chunkmirage}" + assert BY_VERSION.fullmatch(chunkmirage[0].strip()), ( + f"pyproject.toml should take chunkmirage from PyPI by version, not {chunkmirage[0]!r}: " + "PyPI refuses a release whose dependencies name a URL" + ) + commits = LOCK_PIN.findall((ROOT / "web" / "chunkmirage.lock").read_text()) + assert len(commits) == 1, f"web/chunkmirage.lock should pin one full commit, found {commits}" diff --git a/web/ARCHITECTURE.md b/web/ARCHITECTURE.md index 3e7a1471..f7377710 100644 --- a/web/ARCHITECTURE.md +++ b/web/ARCHITECTURE.md @@ -225,10 +225,11 @@ misses one it does, and when the server form's defaults leave ## Finetuning (src/finetune/) The Finetune tab works as on the server with the browser in the server's and -MinIO's places. `volume.ts` (the port of `finetune/session/volume.py`) plans -an annotation volume over the open dataset on the grid the model's -predictions are served on, one chunk per output block, and writes its zarr v2 -metadata (uncompressed chunks, `.` separator) into the paint store; +MinIO's places. `volume.ts` plans an annotation volume over the open dataset +by the server's plan (`cellmap_flow.portable.volume`, through +`Inference.python`) on the grid the model's predictions are served on, one +chunk per output block, and writes its zarr v2 metadata (uncompressed +chunks, `.` separator) into the paint store; `viewer/layers.ts` adds it as the fork's writable segmentation layer on `s3+http(s)://paint//annotation`. Training fits the model's head, the last 1×1 convolution of the ONNX graph: `onnx-head.ts` finds @@ -237,8 +238,10 @@ edits with `onnx-proto.ts`'s field-level protobuf splicing, the head's input tensor as a second graph output and new weights in the head's initializers; `samples.ts` cuts, pads and normalizes each painted chunk's input block exactly as the model stage does (the port of `finetune/data/reader.py`); -`features.ts` runs the model for its logits and features; `targets.ts` (the -port of `target_transforms.py`, parity-tested) makes the targets and masks; +`features.ts` runs the model for its logits and features; `target-call.ts` +has the targets and masks made by `cellmap_flow.portable.targets`, the +Python `target_transforms.py` wraps on the server (`Inference.python`, +above), from the offsets the channel names say; `head-trainer.ts` gathers the painted voxels' features and trains the head from the model's own weights with Adam on a masked, class-balanced loss (BCE-with-logits, MSE or margin: the server's per-voxel losses), plus λ × @@ -256,7 +259,8 @@ the format), which the page that opens it writes back into the base. Which model to start from (`backend/routes/rank.ts`) scores a model on the paint with the engine's own reads: the model served (or the served one reused), `readOutput` over at most `RANK_BLOCKS` of its blocks under the -paint, each painted voxel's model voxel from Label it's `servedIndices`, +paint, each painted voxel's model voxel from Label it's placement +(`cellmap_flow.portable.label_it`, through `label-it/prediction.ts` `place`), and the targets from `targetTransformFor`; then each chunk's labels, targets and logits go to `cellmap_flow.portable.rank` (`Inference.python`, above), where the tallies, the score at the model's own cut-off and the @@ -327,21 +331,24 @@ the server: `backend/routes/ai-annotate.ts` answers the Python routes' JSON accept, reject; Accept's Undo is Label it's, below), and `backend/ai-annotate-job.ts` is the job, one at a time, its result staged in memory until Accept or Reject. -Who does what: `geometry.ts` plans the plane (the depth axis from the -viewer's layout, the raw box, the image laid out as the panel draws it, the -click in image pixels); `plane.ts` reads it from the raw level through -`Inference.readRaw` (clipped to the level and zero past it), resizes it to the image sent, normalizes it to uint8 RGBA (`mask.ts`) -and builds the request; `gemini.ts` is the two Google providers +Who does what: the plane planned (the raw box, the image laid out as the +panel draws it, the click in image pixels), the image sent (resized to the +plan's grid, normalized to uint8), the mask in the model's reply (resized +back, cropped, thresholded by colour), its objects on the write box with the +review's overlay, and the labels Accept writes are the Python both modes +run, `cellmap_flow/portable/ai_annotate.py` (`ai_annotate_calls.py` is what +the page calls); `plane.ts` hands it the plan's inputs and reads the raw box +from the raw level through `Inference.readRaw` (clipped to the level and +zero past it), and builds the request; `mask.ts` hands it the model's image +and the mask; `geometry.ts` keeps the names the viewer's depth axis is read +by; `gemini.ts` is the two Google providers (`gemini_api`: generativelanguage.googleapis.com with `x-goog-api-key`; `vertex_gemini`: aiplatform.googleapis.com, location global, with a bearer token), called straight from the page, with the Python's retries and timeout, and errors in the module's own words, never the service's body; `fake.ts` paints the dark pixels in a disc around the click, for trying the -panel and for the tests; `mask.ts` reads the model's image into a mask by -the colour the prompt asked for and draws the overlay; `labels.ts` and -`fill.ts` are `fill.py` (4-connected objects in the plane, ids seeded from -what the user painted beside, the box written to the paint store whole -chunk by whole chunk, with the undo entry); `prompts.ts` and `organelles.ts` +panel and for the tests; the accepted box is written to the paint store +whole chunk by whole chunk, with the undo entry (`fill.ts`); `prompts.ts` and `organelles.ts` are the catalog; `credentials.ts`, `usage.ts` and `audit.ts` hold the credentials, the daily count (localStorage, 200 a day) and the audit log, one JSON line per event appended to `/ai_annotate_log.jsonl` in the @@ -384,20 +391,29 @@ the same stage object and key, so the chunks the viewer had computed come out of the engine's staged cache; the page reads them over the view's URL through the service worker, as any client reads a view (the engine answers a page's requests through the route it registers, and has no other door to -a chunk), and `infer/output.ts` places them in the box. `prediction.ts` -puts each annotation voxel on the served voxel its centre lies in -(read_prediction: any served model can seed, at its own voxel size); -`segment.ts` turns the output into a probability by its output class and -into objects by threshold and connected components or, for an affinity -model, the mutex watershed (`infer/label.ts` and `infer/affinity.ts`, the -postprocessors' segmenters, already parity-tested against scipy and -mwatershed). The distance watershed and a model's own instances are not -offered: no browser model outputs a distance or instance ids. The ids, the -writes and the undo are `ai-annotate/labels.ts` and `fill.ts` (seedLabels, -relabelObjects for Split Objects, paintBox, rewriteForeground, restoreBox). -One undo stack per volume is the routes', and the AI annotation's Accept -pushes onto it (createRouter hands it over, as `ai_annotate.py` uses -`view_labels._undo_stack`), so one Undo takes back whichever came last. +a chunk), and `infer/output.ts` places them in the box. The seed's maths +is the server's own Python, `cellmap_flow/portable/label_it.py`, called +through `portable/label_it_calls.py` (`Inference.python`, above): the +request's options, the channels it reads, each annotation voxel on the +served voxel its centre lies in (any served model can seed, at its own +voxel size), the output as a probability by its output class, and threshold +and connected components; `label-it/prediction.ts` reads the served box it +names. For an affinity model the Python answers the mutex watershed's input +and `label-it/segment.ts` runs the watershed itself (`infer/affinity.ts`, +parity-tested against mwatershed, which has no Pyodide wheel). scipy, for +the components, loads the first time a seed needs it, not when a dataset +opens. The distance watershed and a model's own instances are not +offered: no browser model outputs a distance or instance ids. The box a +tool covers, the ids a seed or a split gives, which voxels each write +changes and what an undo puts back are the server's Python too, +`cellmap_flow/portable/fill.py`, called through `portable/fill_calls.py` +(so with no dataset open every tool is refused, as a seed is); the routes +read the box from the paint store and write back the box it answers +(`ai-annotate/fill.ts` readBox and writeBox). Split Objects loads scipy as a +seed does, the first time it is needed. One undo stack per volume is the +routes', and the AI annotation's Accept pushes onto it (createRouter hands +it over, as `ai_annotate.py` uses `view_labels._undo_stack`), so one Undo +takes back whichever came last. ## Run on a region (src/run-region/) @@ -423,7 +439,7 @@ volume (the finest level's extent), or typed (`withBox` redraws the box from the fields once typing has paused for `TYPED_BOX_DELAY_MS`); each model's box is the voxels of its served level the region touches (`servedLevel`, the level `infer/engine.ts` serves its view at, voxel 0's centre its OME -translation, as `label-it/prediction.ts` places a seed), and its chunks are +translation, as a seed is placed on it), and its chunks are its output blocks, which the summary counts. `run.ts` runs the models one after another through `Inference.materialize`, with the progress across them, the stop (an AbortSignal), and the provenance under `cellmap_flow` in @@ -495,17 +511,18 @@ web/ routes/{data,pipeline,models,config,finetune,good-regions,view-labels,ai-annotate}.ts state/ openSession(): SessionStore (sessionStorage: the tab is the server's process lifetime); db.ts + local-store.ts for picked directories; paint-store.ts + paint-route.ts: the annotation volumes' files and the worker's paint/ route - finetune/ types.ts (the seams), volume.ts (planVolume, the zarr v2 files), samples.ts, targets.ts, onnx-proto.ts, + finetune/ types.ts (the seams), volume.ts (planVolume, the zarr v2 files), samples.ts, target-call.ts, onnx-proto.ts, onnx-head.ts, features.ts, head-trainer.ts, distillation.ts, good-regions.ts, run.ts (runFinetune), zip.ts (the paint's download), modes.ts (what a manifest offers), augment.ts (the whole network's) full/ the whole network in TF.js: cellmap-tfjs-train.ts (Marwan's runtime, the weights read and written), graph.ts (the training graph, its tile and block sides), net3d.ts (a 3D model's network in a GPU's memory), files.ts, tiles.ts (2D tiles, 3D blocks), tfjs.ts (lazy TF.js), loss.ts (the head's loss in TF.js), run-full.ts - ai-annotate/ AI-assisted annotation (above): types.ts, geometry.ts, plane.ts, mask.ts, labels.ts, fill.ts, prompts.ts, - organelles.ts (the pure ports), images.ts (the canvas codec), gemini.ts, fake.ts, providers.ts, - credentials.ts, usage.ts, audit.ts; labels.ts and fill.ts are Label it's too - label-it/ Label it (above): segment.ts (probability, methods, objects), prediction.ts (the output under a box) + ai-annotate/ AI-assisted annotation (above): types.ts, geometry.ts, plane.ts, mask.ts, fill.ts (the paint store's + box reads and writes, and the undo stack), prompts.ts, organelles.ts, images.ts (the canvas codec), + gemini.ts, fake.ts, providers.ts, credentials.ts, usage.ts, audit.ts; fill.ts is Label it's too + label-it/ Label it (above): segment.ts (the mutex watershed, the probability's call), prediction.ts (the served + level, the placement's call, the output under a box) run-region/ Run on a region (above): region.ts (the box on each model's level, the run_region layer), run.ts (the run, its progress and provenance), output.ts (the folder, the zip, the saved layer), panel.ts (the tab) local-folder/ a folder on this machine: index.ts (the local URL and its key, the pick/drop flow, diff --git a/web/README.md b/web/README.md index 648c4cda..84bd63e5 100644 --- a/web/README.md +++ b/web/README.md @@ -180,8 +180,9 @@ the model runs once per painted chunk on the block the model stage would feed it (`samples.ts`, through the volume's normalizers) for its logits and features (`features.ts`), and the head's weights are trained from the model's own on the painted voxels' features with Adam on a masked, class-balanced -loss (`head-trainer.ts`; the targets are `targets.ts`, the port of -`target_transforms.py`, checked against the Python classes): BCE-with-logits +loss (`head-trainer.ts`; the targets are the server's own, +`cellmap_flow/portable/targets.py` run in the page's Python by +`target-call.ts`): BCE-with-logits by default, or the server's MSE on probabilities or margin loss; Dice, a whole patch's overlap, needs the server. *Voxels per Step* is the head's batch (the server's *Batch Size* counts patches). *Distillation Weight* λ @@ -225,8 +226,9 @@ Centre* reads one plane of raw EM around the point from the open volume (`gemini.ts`: Google's Gemini API with an API key, or Vertex AI with a project and an access token; `fake.ts` paints in the page, for trying it and for the tests), turns the colour it painted into a mask (`mask.ts`) and, on *Accept*, -writes it into the paint store as an object per connected piece (`labels.ts`, -`fill.ts`), which the patch tools' *Undo* takes back. The routes +writes it into the paint store as an object per connected piece (by the +server's own Python, `cellmap_flow/portable/ai_annotate.py` and `fill.py`), +which the patch tools' *Undo* takes back. The routes (`src/backend/routes/ai-annotate.ts`, `src/backend/ai-annotate-job.ts`) answer the Python routes' JSON, so the dashboard's `ai-annotate.js` drives both modes; the credentials pasted into the panel (`credentials.ts`) are used as @@ -246,11 +248,12 @@ of those or of an accepted AI annotation (one stack, 10 deep). A seed reads the model's own output, through its normalizers and none of its postprocessors (`Inference.readOutput`: the model stage served beside the layer, so the chunks already computed for the viewer are reused), placed on -the volume's grid as the server places it (`src/label-it/prediction.ts`), and -segments it by threshold and connected components or, for an affinity model, -the mutex watershed (`src/label-it/segment.ts`, on the segmenters the -postprocessors already use). These are checked against the Python on -fixtures it writes (`test/fixtures/make_label_it_fixtures.py`). The server's +the volume's grid and segmented by threshold and connected components by the +server's own Python (`cellmap_flow/portable/label_it.py`, in the page's +Pyodide worker), or, for an affinity model, by the mutex watershed +(`src/label-it/segment.ts`, on the segmenter the postprocessors already use, +checked against the Python on fixtures it writes, +`test/fixtures/make_label_it_fixtures.py`). The server's distance watershed and a model's own instances are not offered: no model the browser runs outputs a distance or instance ids. diff --git a/web/chunkmirage.lock b/web/chunkmirage.lock index 938c9e02..ae077d29 100644 --- a/web/chunkmirage.lock +++ b/web/chunkmirage.lock @@ -2,4 +2,4 @@ # web/scripts/fetch-chunkmirage.sh clones into web/chunkmirage/ (gitignored). # One line to bump; CI and local builds read the same file. repo=https://github.com/JaneliaSciComp/chunkmirage -sha=7307e36b499f099bb0d5fd80f88cb013c0230387 +sha=a7eb44e62332726c59946aea705ac454ffba9ccd diff --git a/web/src/ai-annotate/fill.ts b/web/src/ai-annotate/fill.ts index 9535ffc9..bc2d6cba 100644 --- a/web/src/ai-annotate/fill.ts +++ b/web/src/ai-annotate/fill.ts @@ -1,34 +1,31 @@ -// Labelling a whole box of an annotation volume at once, in the paint store: -// the port of cellmap_flow/finetune/session/fill.py's box_voxels, paint_box, -// rewrite_foreground and restore_box (and of finetune/session/volume.py's -// volume_corner_nm, which box_voxels places voxels by) for the browser, -// where the volume is the paint store's zarr v2 chunks (finetune/volume.ts) -// instead of MinIO's. An accepted AI annotation writes through paintBox, as -// do the Label it tools' Seed from Prediction and All Background; Split -// Objects through rewriteForeground; Undo, of any of them, is restoreBox -// (backend/routes/view-labels.ts). +// A whole box of an annotation volume read from and written to the paint +// store at once, and the undo stack the box writes push onto: the I/O +// around the box decisions, which are the Python both modes run +// (cellmap_flow/portable/fill.py). The Label it tools +// (backend/routes/view-labels.ts, through portable/fill_calls.py) and an +// accepted AI annotation (backend/ai-annotate-job.ts, through +// portable/ai_annotate_calls.py accept) read the box with readBox, hand it +// to that Python in the engine's worker, write the box it answers with +// writeBox and push the box as it was and as it is (pushUndo), as the +// server's finetune/session/fill.py does around the same functions over +// MinIO. // // What the server does through zarr over MinIO (`arr[box]` read and written) -// is done here chunk by chunk: readBox assembles the box from the chunks it +// is done here chunk by chunk, the volume being the paint store's zarr v2 +// chunks (finetune/volume.ts): readBox assembles the box from the chunks it // touches (a chunk never painted is zeros, zarr's fill value), writeBox puts // each touched chunk back whole, read-modify-write, as the fork's writable -// zarr source does when the brush paints. paintBox, rewriteForeground and -// restoreBox are fill.paint_box, fill.rewrite_foreground and -// fill.restore_box over those two, with the same write rules, the same -// counts and the same "nothing written when nothing changes". +// zarr source does when the brush paints. // // Where voxel 0 is: a volume's `dataset_offset_nm` is voxel 0's CENTRE (the // OME translation, as volume.py's docstring and volume_corner_nm say, and as // finetune/volume.ts planVolume sets it from the served level's origin), so -// its corner is half a voxel before it, and a voxel is in a box when its -// centre is (boxVoxels). +// its corner is half a voxel before it (volumeCornerNm, which the ranking's +// carry and planVolume place the grid by). import type { PaintStore, PaintVolume } from "../contracts"; import { CHUNK_DIR, chunkKey, readPaintedChunk } from "../finetune/volume"; import { type Labels, UNDO_DEPTH, type UndoEntry } from "./types"; -/** The volume fields the box arithmetic reads: its grid. */ -export type VolumeGrid = Pick; - /** The world position of an annotation volume's voxel-0 lower corner, in nm * (volume.volume_corner_nm): `datasetOffsetNm` is voxel 0's centre, so the * corner is half a voxel before it on each axis; zeros when the volume has @@ -37,25 +34,6 @@ export function volumeCornerNm(datasetOffsetNm: readonly number[] | null | undef return outputVoxelSize.map((v, a) => (datasetOffsetNm ? datasetOffsetNm[a] : 0) - v / 2); } -/** `{lo, hi}`: the annotation voxels whose centres lie in a box, clipped to - * the volume (fill.box_voxels). `offsetNm` and `shapeNm` are the box's - * lower corner and size in world nm (z, y, x); `volumeShape` the volume's - * extent in voxels. null when no voxel of the volume is in the box. */ -export function boxVoxels(volume: VolumeGrid, offsetNm: readonly number[], shapeNm: readonly number[], volumeShape: readonly number[]): { lo: number[]; hi: number[] } | null { - const voxel = volume.output_voxel_size; - const corner = volumeCornerNm(volume.dataset_offset_nm, voxel); - const lo: number[] = [], hi: number[] = []; - for (let a = 0; a < 3; a++) { - const start = (offsetNm[a] - corner[a]) / voxel[a]; - const end = start + shapeNm[a] / voxel[a]; - // Voxel i's centre is at i + 0.5 voxels from the corner. - lo.push(Math.max(Math.ceil(start - 0.5), 0)); - hi.push(Math.min(Math.ceil(end - 0.5), volumeShape[a])); - } - if (hi.some((h, a) => h <= lo[a])) return null; - return { lo, hi }; -} - // --------------------------------------------------------------------------- // Boxes of the store's chunks // --------------------------------------------------------------------------- @@ -67,11 +45,6 @@ export function emptyLabels(volume: Pick, n: number): Labe return volume.dtype === "uint16" ? new Uint16Array(n) : new Uint8Array(n); } -/** A zero array of the same kind as `like`. */ -function labelsLike(like: Labels, n: number): Labels { - return like instanceof Uint16Array ? new Uint16Array(n) : new Uint8Array(n); -} - function checkBox(volume: Pick, lo: readonly number[], hi: readonly number[]): void { const shape = volume.dataset_shape_voxels; if (lo.length !== 3 || hi.length !== 3) throw new Error(`a box is three lo and three hi indices, got ${lo.join(",")}..${hi.join(",")}`); @@ -113,7 +86,7 @@ function copyOverlap(volume: Pick, index: readonly nu /** A chunk's labels as the bytes the store holds (the layout labelsOf reads): * uint8 as they are, uint16 little-endian. A copy: the store keeps what it is given. */ -export function chunkBytes(labels: Labels): Uint8Array { +function chunkBytes(labels: Labels): Uint8Array { if (labels instanceof Uint16Array) { const bytes = new Uint8Array(labels.length * 2); const view = new DataView(bytes.buffer); @@ -153,131 +126,9 @@ export async function writeBox(store: PaintStore, volume: PaintVolume, lo: reado } // --------------------------------------------------------------------------- -// paint_box and restore_box +// The undo stack // --------------------------------------------------------------------------- -/** What paintBox did (fill.paint_box's return): how many voxels were written - * with foreground (2 and up) and with background (1), and how many of those - * held a label before. The routes answer these as filled_foreground, - * filled_background and overwritten. */ -export interface PaintCounts { - n_foreground: number; - n_background: number; - n_overwritten: number; -} - -/** `labels` as the volume's dtype (np.asarray(..., dtype=arr.dtype)): a - * value past the dtype wraps as numpy's astype wraps it, which never - * happens for ids from freshIds, which refuses them first. */ -function asDtype(labels: Labels, like: Labels): Labels { - if ((like instanceof Uint16Array) === (labels instanceof Uint16Array)) return labels; - const out = labelsLike(like, labels.length); - for (let i = 0; i < labels.length; i++) out[i] = labels[i]; - return out; -} - -/** Write `labelsFor(existing)` into the box [lo, hi) (fill.paint_box). - * - * `labelsFor` gets the box as the store holds it (readBox) and returns labels - * of the same length (0 for "leave as it is"). Without `overwrite` only - * unannotated voxels (0) are written, as for a seed: what the user painted - * stays. With it every voxel the labels cover is written, painted or not, - * for a result the user has looked at and prefers to what is there; note - * that the accept route (backend/routes/ai-annotate.ts, as the Python's - * ai_annotate_accept) zeroes `labels == 1 & existing != 0` in its own - * labelsFor before this sees them when overwrite is on, so the model's - * background never erases paint; that is the route's rule, not this - * function's, which writes exactly what the labels say. Nothing is written - * when nothing would change (and nothing is pushed on `undo`). When - * something is, `options.undo`, when given, gets `{lo, hi, before, after}` - * pushed (pushUndo: UNDO_DEPTH entries at most) for restoreBox. */ -export async function paintBox( - store: PaintStore, - volume: PaintVolume, - lo: readonly number[], - hi: readonly number[], - labelsFor: (existing: Labels) => Labels, - options: { overwrite: boolean; undo?: UndoEntry[] }, -): Promise { - const existing = await readBox(store, volume, lo, hi); - const labels = asDtype(labelsFor(existing), existing); - if (labels.length !== existing.length) throw new Error(`labelsFor returned ${labels.length} labels for a box of ${existing.length} voxels`); - const write = new Uint8Array(existing.length); - let nWrite = 0, nForeground = 0, nOverwritten = 0; - for (let i = 0; i < existing.length; i++) { - const w = options.overwrite ? labels[i] > 0 && labels[i] !== existing[i] : existing[i] === 0 && labels[i] > 0; - if (!w) continue; - write[i] = 1; - nWrite++; - if (labels[i] >= 2) nForeground++; - if (existing[i] !== 0) nOverwritten++; - } - if (nWrite) { - const before = existing.slice(); - for (let i = 0; i < existing.length; i++) if (write[i]) existing[i] = labels[i]; - await writeBox(store, volume, lo, hi, existing); - if (options.undo) pushUndo(options.undo, { lo: [...lo], hi: [...hi], before, after: existing.slice() }); - } - return { n_foreground: nForeground, n_background: nWrite - nForeground, n_overwritten: nOverwritten }; -} - -/** Write `relabel(existing)`'s labels over the box's foreground voxels - * (fill.rewrite_foreground): Split Objects. - * - * `relabel` gets the box as the store holds it (readBox) and returns the - * labels and its counts (labels.ts relabelObjects); only foreground voxels - * (2 and up) whose label changed are written, so a stroke painted meanwhile - * elsewhere in the box is kept, and nothing is written when none changed. - * Returns how many voxels changed and the counts. `options.undo`: as for - * paintBox. */ -export async function rewriteForeground( - store: PaintStore, - volume: PaintVolume, - lo: readonly number[], - hi: readonly number[], - relabel: (existing: Labels) => { labels: Labels; counts: C }, - options: { undo?: UndoEntry[] } = {}, -): Promise<{ changed: number; counts: C }> { - const existing = await readBox(store, volume, lo, hi); - const { labels: given, counts } = relabel(existing.slice()); - const labels = asDtype(given, existing); - if (labels.length !== existing.length) throw new Error(`relabel returned ${labels.length} labels for a box of ${existing.length} voxels`); - const changed = new Uint8Array(existing.length); - let n = 0; - for (let i = 0; i < existing.length; i++) { - if (existing[i] >= 2 && labels[i] !== existing[i]) { - changed[i] = 1; - n++; - } - } - if (n) { - const before = existing.slice(); - for (let i = 0; i < existing.length; i++) if (changed[i]) existing[i] = labels[i]; - await writeBox(store, volume, lo, hi, existing); - if (options.undo) pushUndo(options.undo, { lo: [...lo], hi: [...hi], before, after: existing.slice() }); - } - return { changed: n, counts }; -} - -/** Put the box back to `entry.before` where it still holds `entry.after` - * (fill.restore_box): an undo. Only voxels the action changed and nobody has - * touched since go back, so a stroke painted in the box after the action is - * kept. Returns how many voxels were restored. */ -export async function restoreBox(store: PaintStore, volume: PaintVolume, entry: UndoEntry): Promise { - const { lo, hi, before, after } = entry; - const current = await readBox(store, volume, lo, hi); - if (before.length !== current.length || after.length !== current.length) throw new Error(`the undo entry's ${before.length} voxels are not the box's ${current.length}`); - let restored = 0; - for (let i = 0; i < current.length; i++) { - if (before[i] !== after[i] && current[i] === after[i]) { - current[i] = before[i]; - restored++; - } - } - if (restored) await writeBox(store, volume, lo, hi, current); - return restored; -} - /** Push `entry` on an undo stack that keeps the last UNDO_DEPTH entries (the * server's deque(maxlen=UNDO_DEPTH) per volume): the oldest goes when it is full. */ export function pushUndo(stack: UndoEntry[], entry: UndoEntry): void { diff --git a/web/src/ai-annotate/gemini.ts b/web/src/ai-annotate/gemini.ts index e66aa6df..2263947c 100644 --- a/web/src/ai-annotate/gemini.ts +++ b/web/src/ai-annotate/gemini.ts @@ -20,9 +20,11 @@ // The flow is vertex_gemini.py's: the plane padded to a square with its // median grey (images.ts padToSquare), sent as a PNG with the prompt and // response modalities IMAGE and TEXT; the answer's last image that is not a -// `thought` part decoded (images.ts: the checks), resized to the sent size -// when the model answered at another, the padding cropped off, and the -// pixels of the asked-for colour taken as the mask (mask.ts extractMask). +// `thought` part decoded (images.ts: the checks), then, by the Python both +// modes run (mask.ts modelMask: portable/ai_annotate.py model_mask, which the +// server's backend calls too), resized to the sent size when the model +// answered at another, the padding cropped off, and the pixels of the +// asked-for colour taken as the mask. // Only "try again later" answers (429, 503, or an error body whose status is // UNAVAILABLE or RESOURCE_EXHAUSTED) are retried, at most MAX_ATTEMPTS // attempts in all with the waits RETRY_WAITS_MS; everything else fails at @@ -34,8 +36,8 @@ // error.details[].reason, blockReason, finishReason) and the model's own // text, stripped of control characters and cut to MAX_REPLY_CHARS, are read // into a message, as vertex_gemini.py does. Nothing is logged. -import { cropTo, padToSquare } from "./images"; -import { extractMask } from "./mask"; +import { padToSquare } from "./images"; +import { modelMask } from "./mask"; import { AIAnnotateError, CALL_TIMEOUT_MS, @@ -125,16 +127,14 @@ export function createGeminiProvider(type: GeminiType, deps: GeminiDeps): Provid const { image, text, mimeType } = imageAndText(answer); if (image === null) throw new AIAnnotateError("bad_response", noImageMessage(answer, text)); - let modelImage = await deps.codec.decode(imageBytes(image), mimeType); - if (modelImage.width !== sent.width || modelImage.height !== sent.height) { - // The model answered at another size (or shape: stretching it then - // is what the preview shows). Back to the sent size, then the - // padding off: what is left lines up with the plane. - modelImage = await deps.codec.resize(modelImage, sent.width, sent.height, true); - } - modelImage = cropTo(modelImage, request.image.width, request.image.height); - const mask = extractMask(modelImage, request.target_rgb); - return { mask, model_image: modelImage, model, usage: usageOf(answer) } satisfies SegmentResult; + const reply = await deps.codec.decode(imageBytes(image), mimeType); + // A reply at another size (or shape: stretching it then is what the + // preview shows) goes back to the sent size, then the padding comes + // off: what is left lines up with the plane, and its pixels of the + // target colour are the mask. + if (!options.python) throw new Error("the Gemini provider takes its mask with the engine's Python: open a dataset first"); + const { mask, model_image } = await modelMask(options.python, reply, sent, request.image, request.target_rgb); + return { mask, model_image, model, usage: usageOf(answer) } satisfies SegmentResult; }, async listModels(credentials, options = {}) { diff --git a/web/src/ai-annotate/geometry.ts b/web/src/ai-annotate/geometry.ts index b9158262..285050cd 100644 --- a/web/src/ai-annotate/geometry.ts +++ b/web/src/ai-annotate/geometry.ts @@ -1,84 +1,36 @@ -// Which plane of the data an AI annotation reads, and which voxels it -// writes: the port of cellmap_flow/ai_annotate/geometry.py (screen_shape, -// to_screen, from_screen, PlanePlan's properties, plan_plane, raw_box) for -// the browser. The viewer side of that module, depth_axis_from_layout and -// depth_axis_for_view, is viewer/state.ts's (depthAxisFromLayout), which -// reads the page's Neuroglancer; the `volume` here is a PaintVolume record -// (finetune/volume.ts) and the raw grid readPlane (plane.ts) passes rawBox is -// a DatasetInfo level's. -// -// A click (or the view centre) picks a point in world nm and the plane the -// user is looking at picks the depth axis. planPlane turns those into a -// PlanePlan: a square field of view around the point, snapped to the -// annotation volume's voxels and clipped to the volume (the write box, one -// annotation voxel thick along the depth axis), and the raw read that covers -// the same physical area at the model's input voxel size (one input voxel -// thick, at the point's depth). So whatever the model paints on the image -// lands on the annotation voxels under it. -// -// Where voxel 0 is (the conclusion the plane read rests on; plane.ts has the -// raw side): the volume's `dataset_offset_nm` is voxel 0's CENTRE, the OME -// translation, which is how volume.py reads it (volume_corner_nm, fill.ts -// here) and how finetune/volume.ts planVolume sets it (the served output -// level's origin, outputLevel: the raw level's corner plus half an output -// voxel, itself derived from the raw origin chunkmirage reads from the OME -// translation, ome.ts openImage). A voxel is in a box when its centre is -// (fill.boxVoxels). The raw is read on its own grid from the voxel that -// holds the first pixel's centre (rawBox). -// -// Axes are z, y, x throughout, as everywhere in cellmap-flow, except in the -// image sent: that is laid out as the viewer's panel shows the plane -// (toScreen), so the model, and the user reviewing its answer, see what was -// on screen. Which axis runs down it and which across, and whether across -// runs backwards, depend on the viewer's display dimensions (screenAxes): -// cellmap-flow's viewers show x, y, z, so the XY plane is drawn as in z, y, x -// order, the XZ plane too, and the YZ plane transposed and mirrored. -// -// Arithmetic follows the Python operation by operation (snapIntegral where -// it snaps, numpy's round-half-to-even where it rounds), so a plan made here -// is the plan the server would make for the same volume and point; the -// parity fixture (test/fixtures/ai_annotate.json) checks that. -import type { PaintVolume } from "../contracts"; -import { boxVoxels, volumeCornerNm } from "./fill"; -import { AIAnnotateError, type DepthAxis, MAX_IMAGE_SIDE, PLANE_NAMES, type PlanePlan } from "./types"; +// What the browser keeps of cellmap_flow/ai_annotate/geometry.py: the names +// the viewer's side reads the plane by (viewer/state.ts depthAxisFromLayout, +// which reads the page's Neuroglancer as the server's depth_axis_from_layout +// reads its viewer) and Python's `:g` number format, which the prompt spells +// its resolution with (prompts.ts). Which plane is read and which voxels are +// written is the Python both modes run (cellmap_flow/portable/ai_annotate.py +// plan_plane, called from plane.ts). export const AXIS_NAMES: readonly ["z", "y", "x"] = ["z", "y", "x"]; -/** The volume fields a plan reads. */ -export type PlanVolume = Pick; - -// --------------------------------------------------------------------------- -// numpy's arithmetic -// --------------------------------------------------------------------------- - -/** metadata.snap_integral's tolerance: a value this close (relative, at - * least absolute) to a whole number is that number. */ -const INTEGRAL_TOLERANCE = 1e-6; - -/** `value` with a near-whole number made exactly whole (io/metadata.py - * snap_integral): voxel arithmetic in nm leaves 511.99999999 where 512 is - * meant, and a floor or a round after it must not see the difference. */ -export function snapIntegral(value: number): number { - const rounded = Math.round(value); - return Math.abs(value - rounded) <= INTEGRAL_TOLERANCE * Math.max(1, Math.abs(value)) ? rounded : value; -} +/** Neuroglancer names its cross-section panels after display-dimension + * *slots*, not after the dimensions' names: the "xy" panel shows display + * dimensions 0 and 1 and looks along 2, "xz" (rotated about the first) + * looks along 1, "yz" (rotated about the second) along 0 + * (data_panel_layout.ts's AXES_RELATIVE_ORIENTATION). The display dimensions + * are the first three of the viewer's coordinate space unless set. + * cellmap-flow's viewers order their dimensions x, y, z (viewer/layers.ts + * VIEWER_AXES), so the "xy" panel is the XY plane, looking along z. A viewer + * in z, y, x order (a link made before) has a z-y slice looking along x + * there, and the XY plane is its "yz" panel (geometry.py _LAYOUT_DEPTH_SLOT). */ +export const LAYOUT_DEPTH_SLOT: Readonly> = { xy: 2, xz: 1, yz: 0 }; -/** numpy's np.round: to the nearest whole number, halves to the even one - * (Math.round takes them up). */ -export function roundHalfEven(value: number): number { - const floor = Math.floor(value); - const diff = value - floor; - if (diff < 0.5) return floor; - if (diff > 0.5) return floor + 1; - return floor % 2 === 0 ? floor : floor + 1; +/** The data axis (0 z, 1 y, 2 x) display slot `slot` shows, or -1 when it shows none of them. */ +export function slotAxis(names: readonly string[], slot: number): number { + return slot < names.length ? (AXIS_NAMES as readonly string[]).indexOf(names[slot]) : -1; } /** Python's `format(value, "g")` with `precision` significant digits (6, as * `f"{x:g}"`): the shortest of fixed and scientific notation, trailing zeros - * dropped, the exponent at least two digits; the messages here and the - * prompt's resolution sentence spell numbers with it. A value exactly - * halfway at the last digit may round the other way than Python (which - * rounds half to even where JavaScript's toExponential rounds half up). */ + * dropped, the exponent at least two digits; the prompt's resolution + * sentence spells its number with it. A value exactly halfway at the last + * digit may round the other way than Python (which rounds half to even + * where JavaScript's toExponential rounds half up). */ export function formatG(value: number, precision = 6): string { if (Number.isNaN(value)) return "nan"; if (!Number.isFinite(value)) return value > 0 ? "inf" : "-inf"; @@ -101,291 +53,3 @@ export function formatG(value: number, precision = 6): string { function stripZeros(text: string): string { return text.includes(".") ? text.replace(/0+$/, "").replace(/\.$/, "") : text; } - -// --------------------------------------------------------------------------- -// Screen layout (geometry.py _SCREEN, screen_shape, to_screen, from_screen) -// --------------------------------------------------------------------------- - -/** Neuroglancer names its cross-section panels after display-dimension - * *slots*, not after the dimensions' names: the "xy" panel shows display - * dimensions 0 and 1 and looks along 2, "xz" (rotated about the first) - * looks along 1, "yz" (rotated about the second) along 0 - * (data_panel_layout.ts's AXES_RELATIVE_ORIENTATION). The display dimensions - * are the first three of the viewer's coordinate space unless set. - * cellmap-flow's viewers order their dimensions x, y, z (viewer/layers.ts - * VIEWER_AXES), so the "xy" panel is the XY plane, looking along z. A viewer - * in z, y, x order (a link made before) has a z-y slice looking along x - * there, and the XY plane is its "yz" panel (geometry.py _LAYOUT_DEPTH_SLOT). */ -export const LAYOUT_DEPTH_SLOT: Readonly> = { xy: 2, xz: 1, yz: 0 }; - -/** How Neuroglancer's panels draw their plane, by the display-dimension - * slot each looks along (geometry.py _PANEL_SLOTS): the slot drawn down, the - * slot drawn across, and whether across runs backwards. The "xy" panel - * (along slot 2) draws display dimension 0 across and 1 down; "xz" (along 1) - * turns it a quarter about the first, so 0 across and 2 down; "yz" (along 0) - * a quarter about the second, so 1 down and 2 across, increasing to the left - * (data_panel_layout.ts's AXES_RELATIVE_ORIENTATION quaternions). */ -const PANEL_SLOTS: Record = { - 2: { down: 1, across: 0, backwards: false }, - 1: { down: 2, across: 0, backwards: false }, - 0: { down: 1, across: 2, backwards: true }, -}; - -/** `displayDimensions` when they are z, y and x in some order, else z, y, x - * (geometry.display_names): the display dimensions of a viewer that sets - * none over cellmap-flow's coordinate space, and of a plan saved before - * plans kept them. */ -export function displayNames(displayDimensions?: readonly string[] | null): string[] { - const names = [...(displayDimensions ?? [])]; - const same = [...names].sort().join() === [...AXIS_NAMES].sort().join(); - return same ? names : [...AXIS_NAMES]; -} - -/** The data axis (0 z, 1 y, 2 x) display slot `slot` shows, or -1 when it shows none of them. */ -export function slotAxis(names: readonly string[], slot: number): number { - return slot < names.length ? (AXIS_NAMES as readonly string[]).indexOf(names[slot]) : -1; -} - -/** The data axes (0 z, 1 y, 2 x) the panel looking along `depthAxis` draws - * down and across, and whether across runs backwards, in a viewer whose - * display dimensions are `displayDimensions` (displayNames; - * geometry.screen_axes). */ -export function screenAxes(depthAxis: DepthAxis, displayDimensions?: readonly string[] | null): { down: number; across: number; backwards: boolean } { - const names = displayNames(displayDimensions); - const { down, across, backwards } = PANEL_SLOTS[names.indexOf(AXIS_NAMES[checkDepthAxis(depthAxis)]) as DepthAxis]; - return { down: slotAxis(names, down), across: slotAxis(names, across), backwards }; -} - -function screen(depthAxis: DepthAxis, displayDimensions?: readonly string[] | null): { transposed: boolean; backwards: boolean } { - const { down, across, backwards } = screenAxes(depthAxis, displayDimensions); - return { transposed: down > across, backwards }; -} - -function checkDepthAxis(depthAxis: number): DepthAxis { - if (depthAxis !== 0 && depthAxis !== 1 && depthAxis !== 2) throw new Error(`depth_axis must be 0, 1 or 2 (z, y, x), got ${JSON.stringify(depthAxis)}`); - return depthAxis; -} - -/** A plane's (rows, cols) in z, y, x order, as the panel draws it (screenAxes). */ -export function screenShape(shape: readonly number[], depthAxis: DepthAxis, displayDimensions?: readonly string[] | null): [number, number] { - const { transposed } = screen(depthAxis, displayDimensions); - const [rows, cols] = shape; - return transposed ? [cols, rows] : [rows, cols]; -} - -/** The typed arrays a plane may come as. */ -export type PlaneArray = Uint8Array | Uint16Array | Uint32Array | Int32Array | Float32Array | Float64Array; - -/** A 2D array, rows first (C order): `data` has `rows × cols` values. */ -export interface Plane { - rows: number; - cols: number; - data: T; -} - -function sameKind(like: T, n: number): T { - return new (like.constructor as new (n: number) => T)(n); -} - -/** `plane` (rows × cols, its two axes in z, y, x order) as the panel looking - * along `depthAxis` draws it in a viewer showing `displayDimensions` - * (geometry.to_screen, screenAxes): transposed when the axis drawn down is - * the later one in z, y, x order, then mirrored across when across runs - * backwards. */ -export function toScreen(plane: T, rows: number, cols: number, depthAxis: DepthAxis, displayDimensions?: readonly string[] | null): Plane { - const { transposed, backwards } = screen(depthAxis, displayDimensions); - if (plane.length !== rows * cols) throw new Error(`a ${rows}×${cols} plane has ${rows * cols} values, not ${plane.length}`); - const outRows = transposed ? cols : rows, outCols = transposed ? rows : cols; - const out = sameKind(plane, plane.length); - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const row = transposed ? c : r; - let col = transposed ? r : c; - if (backwards) col = outCols - 1 - col; - out[row * outCols + col] = plane[r * cols + c]; - } - } - return { rows: outRows, cols: outCols, data: out }; -} - -/** toScreen undone (geometry.from_screen): an image as the panel draws it - * (`rows × cols` as on screen), in z, y, x order. */ -export function fromScreen(image: T, rows: number, cols: number, depthAxis: DepthAxis, displayDimensions?: readonly string[] | null): Plane { - const { transposed, backwards } = screen(depthAxis, displayDimensions); - if (image.length !== rows * cols) throw new Error(`a ${rows}×${cols} image has ${rows * cols} pixels, not ${image.length}`); - const outRows = transposed ? cols : rows, outCols = transposed ? rows : cols; - const out = sameKind(image, image.length); - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const col = backwards ? cols - 1 - c : c; - const index = transposed ? col * outCols + r : r * outCols + col; - out[index] = image[r * cols + c]; - } - } - return { rows: outRows, cols: outCols, data: out }; -} - -// --------------------------------------------------------------------------- -// A plan's properties (PlanePlan.plane_axes, plane_name, write_shape, click_px) -// --------------------------------------------------------------------------- - -/** The two in-plane axes, in z, y, x order. */ -export function planeAxes(plan: Pick): [number, number] { - return [0, 1, 2].filter((a) => a !== plan.depth_axis) as [number, number]; -} - -/** "XY", "XZ" or "YZ": the data axes the plane shows. */ -export function planeName(plan: Pick): string { - return PLANE_NAMES[plan.depth_axis]; -} - -/** The write box's in-plane shape in annotation voxels, its planeAxes in z, y, x order. */ -export function writeShape(plan: Pick): [number, number] { - return planeAxes(plan).map((a) => plan.write_hi[a] - plan.write_lo[a]) as [number, number]; -} - -/** (row, col) of the point in the image sent (PlanePlan.click_px): the - * pixel the point's fraction of the raw box falls in along the axis drawn - * down and the axis drawn across, clipped to the image; mirrored when across - * runs backwards, counted from the near end first so a point on a boundary - * takes the pixel it takes in z, y, x order. */ -export function clickPx(plan: PlanePlan): [number, number] { - const { down, across, backwards } = screenAxes(plan.depth_axis, plan.display_dimensions); - const position = [down, across].map((a, i) => { - const side = plan.image_shape[i]; - const fraction = (plan.point_nm[a] - plan.raw_offset_nm[a]) / plan.raw_shape_nm[a]; - return Math.min(Math.max(Math.floor(fraction * side), 0), side - 1); - }); - if (backwards) position[1] = plan.image_shape[1] - 1 - position[1]; - return [position[0], position[1]]; -} - -// --------------------------------------------------------------------------- -// plan_plane and raw_box -// --------------------------------------------------------------------------- - -/** `values` as Python's "a, b, c" of `:g` numbers. */ -const joinG = (values: readonly number[]) => values.map((v) => formatG(v)).join(", "); - -/** The plane through `pointNm` (world nm, z, y, x) across `depthAxis` - * (geometry.plan_plane). - * - * `volume` is the annotation volume's record (output_voxel_size, - * input_voxel_size, dataset_offset_nm), `volumeShape` its labels' shape - * (dataset_shape_voxels). The field of view is square in nm, `cropSizePx` - * annotation voxels along the finer in-plane axis (both, for isotropic - * voxels), centred on the point; its write box is the annotation voxels - * whose centres lie in it, clipped to the volume, in the annotation voxel - * that holds the point along the depth axis. - * - * `dataBoxNm` is the raw data's extent, [lo, hi] world nm z, y, x - * (plane.ts rawExtentNm): the field is clipped to it too. The volume is the - * data rounded up to whole chunks of the model's output, which for Cellpose - * at 64 nm is up to 33 um past the data's edge, and there the plane is only - * the read's zero padding: an image of it was labelled as background, and - * the cells beside it smeared into it. - * - * The raw is read over the clipped box's extent, so the image and the box - * cover the same physical area. The image's pixels are the finer in-plane - * input voxel size, square; when that makes a side longer than - * MAX_IMAGE_SIDE the pixels are made larger to fit. The image is laid out as - * the viewer's panel draws the plane, for the viewer's `displayDimensions` - * (viewer.viewDisplayDimensions; z, y, x when null). - * - * Throws AIAnnotateError("refused", ..., 400) when the point is outside the - * volume or the data: a field clipped to an edge it does not reach is not - * what was aimed at. A bad depth axis, crop size or point is a plain Error - * (the Python's ValueError). */ -export function planPlane( - volume: PlanVolume, - volumeShape: readonly number[], - pointNm: readonly number[], - depthAxis: DepthAxis, - cropSizePx: number, - dataBoxNm: readonly [readonly number[], readonly number[]] | null = null, - displayDimensions: readonly string[] | null = null, -): PlanePlan { - checkDepthAxis(depthAxis); - if (!Number.isFinite(cropSizePx) || Math.trunc(cropSizePx) < 1) throw new Error(`crop_size_px must be at least 1, got ${JSON.stringify(cropSizePx)}`); - if (pointNm.length !== 3 || pointNm.some((p) => typeof p !== "number" || !Number.isFinite(p))) { - throw new Error(`point_nm must be three finite numbers (z, y, x), got ${JSON.stringify(pointNm)}`); - } - const output = volume.output_voxel_size.map(Number), input = volume.input_voxel_size.map(Number); - const shape = volumeShape.map((n) => Math.trunc(n)); - const corner = volumeCornerNm(volume.dataset_offset_nm, output); - const point = pointNm.map(Number); - - const voxel = point.map((p, a) => Math.floor(snapIntegral((p - corner[a]) / output[a]))); - if (voxel.some((v, a) => v < 0 || v >= shape[a])) { - const extent = corner.map((c, a) => `${formatG(c)}..${formatG(c + shape[a] * output[a])}`); - throw new AIAnnotateError( - "refused", - `The point (${joinG(point)}) nm is outside the annotation volume (z, y, x ${extent.join(", ")} nm): move the view onto the data and try again.`, - 400, - ); - } - - const plane = planeAxes({ depth_axis: depthAxis }); - const sideNm = Math.trunc(cropSizePx) * Math.min(...plane.map((a) => output[a])); - const offset = point.map((p) => p - sideNm / 2); - const size = [sideNm, sideNm, sideNm]; - if (dataBoxNm) { - const [dataLo, dataHi] = dataBoxNm.map((v) => v.map(Number)); - if (point.some((p, a) => p < dataLo[a] || p >= dataHi[a])) { - const extent = dataLo.map((lo, a) => `${formatG(lo)}..${formatG(dataHi[a])}`); - throw new AIAnnotateError( - "refused", - `The point (${joinG(point)}) nm is outside the data (z, y, x ${extent.join(", ")} nm): move the view onto the data and try again.`, - 400, - ); - } - for (const a of plane) { - const top = Math.min(offset[a] + size[a], dataHi[a]); - offset[a] = Math.max(offset[a], dataLo[a]); - size[a] = top - offset[a]; - } - } - // Along the depth axis, exactly the voxel that holds the point: a box one - // voxel long whose only centre is that voxel's. - offset[depthAxis] = corner[depthAxis] + voxel[depthAxis] * output[depthAxis]; - size[depthAxis] = output[depthAxis]; - const box = boxVoxels(volume, offset, size, shape); - if (!box) throw new AIAnnotateError("refused", "No annotation voxels lie around the point.", 400); // cannot happen with the point inside, but never write an empty box - const { lo, hi } = box; - - const rawOffset = lo.map((l, a) => corner[a] + l * output[a]); - const rawShape = hi.map((h, a) => (h - lo[a]) * output[a]); - rawOffset[depthAxis] = point[depthAxis] - input[depthAxis] / 2; - rawShape[depthAxis] = input[depthAxis]; - - const extent = plane.map((a) => rawShape[a]); - const resolution = Math.max(Math.min(...plane.map((a) => input[a])), Math.max(...extent) / MAX_IMAGE_SIDE); - const pixels = extent.map((e) => Math.max(roundHalfEven(snapIntegral(e / resolution)), 1)); - const display = displayNames(displayDimensions); - return { - depth_axis: depthAxis, - point_nm: point, - raw_offset_nm: rawOffset.map(snapIntegral), - raw_shape_nm: rawShape.map(snapIntegral), - image_shape: screenShape(pixels, depthAxis, display), - input_voxel_size: input, - write_lo: lo, - write_hi: hi, - resolution_nm: snapIntegral(resolution), - display_dimensions: display, - }; -} - -/** The raw voxels to read for `plan`, on a raw grid of `voxelSize` from - * `corner` (the lower corner of its voxel 0, in nm; geometry.raw_box): - * `begin` and `count` per axis (io/geometry.py's Box). - * - * Each axis starts at the voxel that holds the first sample's centre (half - * a voxel in from the box's corner) and runs for as many voxels as fit in - * the box, at least one: so a box on the grid reads exactly its voxels, and - * the depth axis reads the voxel that holds the point. */ -export function rawBox(plan: Pick, voxelSize: readonly number[], corner: readonly number[]): { begin: number[]; count: number[] } { - const begin = plan.raw_offset_nm.map((o, a) => Math.floor(snapIntegral((o + voxelSize[a] / 2 - corner[a]) / voxelSize[a]))); - const count = plan.raw_shape_nm.map((s, a) => Math.max(roundHalfEven(snapIntegral(s / voxelSize[a])), 1)); - return { begin, count }; -} diff --git a/web/src/ai-annotate/images.ts b/web/src/ai-annotate/images.ts index 3e67364f..6df96de9 100644 --- a/web/src/ai-annotate/images.ts +++ b/web/src/ai-annotate/images.ts @@ -1,6 +1,7 @@ // Images for AI-assisted annotation: encoding the plane that is sent, checking -// and decoding the image a model sends back, and the two pure helpers the -// Gemini provider needs to line the answer up with the plane. The port of +// and decoding the image a model sends back, and the padding the Gemini +// provider sends the plane with (lining its answer up with the plane again +// is the Python's: mask.ts modelMask). The port of // cellmap_flow/ai_annotate/images.py (decode_model_image: the checks a // model's image must pass before it is decoded) and of vertex_gemini.py's // _square, for the browser, where the canvas is the only image codec there @@ -23,8 +24,8 @@ // where there is no canvas. It writes no real PNG: its bytes are a tiny // container of the raw RGBA pixels (see RAW_CONTAINER_TAG), prefixed with // the PNG signature so that the checks above treat them as a PNG and the -// Gemini provider's whole flow (encode, send, decode, resize, crop) runs -// unchanged in a test. It cannot decode a real PNG. +// Gemini provider's whole flow (encode, send, decode) runs unchanged in a +// test. It cannot decode a real PNG. import { AIAnnotateError, ALLOWED_IMAGE_TYPES, type ImageCodec, MAX_IMAGE_PIXELS, type RgbaImage } from "./types"; // --------------------------------------------------------------------------- @@ -123,7 +124,7 @@ export function medianOfBytes(values: Uint8Array): number { } // --------------------------------------------------------------------------- -// Lining a model's answer up with the plane (vertex_gemini.py _square and the crop) +// The plane padded square for the model (vertex_gemini.py _square) // --------------------------------------------------------------------------- /** `image` padded on the right or bottom to a square with its median grey @@ -134,7 +135,7 @@ export function medianOfBytes(values: Uint8Array): number { * square, and resizing the answer back to it would stretch the mask. The * padding is a flat grey rather than black, which the prompt asks the model * to paint everything else, so it reads as nothing rather than as a - * structure's dark edge; the provider crops it off the answer (cropTo). The + * structure's dark edge; the provider crops it off the answer (mask.ts modelMask). The * grey is the integer part of the median (int(np.median(...))). A square * image comes back as it is. */ export function padToSquare(image: RgbaImage): { image: RgbaImage; padded: boolean } { @@ -153,22 +154,6 @@ export function padToSquare(image: RgbaImage): { image: RgbaImage; padded: boole return { image: { width: side, height: side, data }, padded: true }; } -/** The top-left `width` × `height` of `image` (PIL's crop((0, 0, width, - * height))), which drops the padding padToSquare added once the model's - * image is back at the sent size. The image returned is `image` itself when - * nothing is cropped. A box reaching past the image is a programming error. */ -export function cropTo(image: RgbaImage, width: number, height: number): RgbaImage { - if (width > image.width || height > image.height) { - throw new Error(`cannot crop a ${image.width}×${image.height} image to ${width}×${height}`); - } - if (width === image.width && height === image.height) return image; - const data = new Uint8ClampedArray(width * height * 4); - for (let row = 0; row < height; row++) { - data.set(image.data.subarray(row * image.width * 4, row * image.width * 4 + width * 4), row * width * 4); - } - return { width, height, data }; -} - // --------------------------------------------------------------------------- // The page's codec: the canvas // --------------------------------------------------------------------------- @@ -193,7 +178,7 @@ function makeCanvas(width: number, height: number): { canvas: OffscreenCanvas | return { canvas, ctx }; } -/** `image` drawn on a fresh canvas, for encoding or as the source of a resize. */ +/** `image` drawn on a fresh canvas, for encoding. */ function canvasOf(image: RgbaImage): { canvas: OffscreenCanvas | HTMLCanvasElement; ctx: Canvas2D } { const made = makeCanvas(image.width, image.height); // ImageData wants a Uint8ClampedArray over an ArrayBuffer of its own; the copy also keeps the caller's pixels untouched. @@ -213,8 +198,7 @@ function readPixels(ctx: Canvas2D, width: number, height: number): RgbaImage { /** The ImageCodec the page uses: PNG encoding through the canvas's own * encoder, decoding through createImageBitmap (PNG, JPEG and WebP, as every - * browser decodes them), resizing by drawImage with the browser's smoothing - * on (bilinear or better) or off (nearest neighbour). */ + * browser decodes them). */ export function canvasCodec(): ImageCodec { return { async encodePng(image) { @@ -245,16 +229,6 @@ export function canvasCodec(): ImageCodec { bitmap.close(); } }, - - async resize(image, width, height, smooth) { - if (width === image.width && height === image.height) return image; - const source = canvasOf(image); - const { ctx } = makeCanvas(width, height); - ctx.imageSmoothingEnabled = smooth; - if (smooth) ctx.imageSmoothingQuality = "high"; - ctx.drawImage(source.canvas, 0, 0, image.width, image.height, 0, 0, width, height); - return readPixels(ctx, width, height); - }, }; } @@ -271,10 +245,9 @@ const RAW_HEADER_BYTES = 8 + 4 + 4 + 4; /** A pure-JavaScript ImageCodec for the unit tests and Node: `encodePng` * writes the raw container described at RAW_CONTAINER_TAG (not a PNG, though * it begins with one's signature so that checkBeforeDecode treats it as - * one), `decode` reads it back after the same checks the canvas codec makes, - * and `resize` is a nearest-neighbour or bilinear resampler in JavaScript. - * It cannot decode a real PNG, JPEG or WebP: those fail as "could not be - * decoded". */ + * one), and `decode` reads it back after the same checks the canvas codec + * makes. It cannot decode a real PNG, JPEG or WebP: those fail as "could + * not be decoded". */ export function jsCodec(): ImageCodec { return { async encodePng(image) { @@ -301,60 +274,9 @@ export function jsCodec(): ImageCodec { return { width, height, data: new Uint8ClampedArray(bytes.subarray(RAW_HEADER_BYTES)) }; }, - async resize(image, width, height, smooth) { - if (width === image.width && height === image.height) return image; - return smooth ? resizeBilinear(image, width, height) : resizeNearest(image, width, height); - }, }; } -/** `image` at `width` × `height`, each pixel the source pixel whose centre - * is nearest (pixel centres at + 0.5, as PIL's NEAREST maps them). */ -export function resizeNearest(image: RgbaImage, width: number, height: number): RgbaImage { - const data = new Uint8ClampedArray(width * height * 4); - const sx = image.width / width, sy = image.height / height; - for (let y = 0; y < height; y++) { - const srcY = Math.min(image.height - 1, Math.floor((y + 0.5) * sy)); - for (let x = 0; x < width; x++) { - const srcX = Math.min(image.width - 1, Math.floor((x + 0.5) * sx)); - const from = (srcY * image.width + srcX) * 4, to = (y * width + x) * 4; - data[to] = image.data[from]; - data[to + 1] = image.data[from + 1]; - data[to + 2] = image.data[from + 2]; - data[to + 3] = image.data[from + 3]; - } - } - return { width, height, data }; -} - -/** `image` at `width` × `height` by bilinear interpolation between the four - * nearest source pixel centres (centres aligned: a destination centre maps - * to (x + 0.5) × scale − 0.5 in the source, clamped at the edges), rounded - * to the nearest byte. Plain interpolation, not PIL's wider antialiasing - * filter when shrinking: close enough for lining a model's image up with - * the plane, which is what the Gemini provider needs it for. */ -export function resizeBilinear(image: RgbaImage, width: number, height: number): RgbaImage { - const data = new Uint8ClampedArray(width * height * 4); - const sx = image.width / width, sy = image.height / height; - const src = image.data, sw = image.width; - for (let y = 0; y < height; y++) { - const fy = Math.min(Math.max((y + 0.5) * sy - 0.5, 0), image.height - 1); - const y0 = Math.floor(fy), y1 = Math.min(y0 + 1, image.height - 1), wy = fy - y0; - for (let x = 0; x < width; x++) { - const fx = Math.min(Math.max((x + 0.5) * sx - 0.5, 0), image.width - 1); - const x0 = Math.floor(fx), x1 = Math.min(x0 + 1, image.width - 1), wx = fx - x0; - const p00 = (y0 * sw + x0) * 4, p01 = (y0 * sw + x1) * 4, p10 = (y1 * sw + x0) * 4, p11 = (y1 * sw + x1) * 4; - const to = (y * width + x) * 4; - for (let c = 0; c < 4; c++) { - const top = src[p00 + c] * (1 - wx) + src[p01 + c] * wx; - const bottom = src[p10 + c] * (1 - wx) + src[p11 + c] * wx; - data[to + c] = Math.round(top * (1 - wy) + bottom * wy); - } - } - } - return { width, height, data }; -} - // --------------------------------------------------------------------------- // Bytes // --------------------------------------------------------------------------- diff --git a/web/src/ai-annotate/labels.ts b/web/src/ai-annotate/labels.ts deleted file mode 100644 index 7969ed76..00000000 --- a/web/src/ai-annotate/labels.ts +++ /dev/null @@ -1,227 +0,0 @@ -// The ids a box of labels gets from a segmentation: the port of -// cellmap_flow/finetune/session/fill.py's _fresh_ids, seed_labels and -// relabel_objects, and of the scipy.ndimage.label call -// pipeline.mask_to_write_shape makes on the mask (label2d), for the browser's AI -// annotation and the Finetune tab's Label it tools (Seed from Prediction's -// ids, Split Objects' relabelling; backend/routes/view-labels.ts). The -// connected-component labelling itself is infer/label.ts's (labelComponents, -// which numbers objects as scipy does); for an AI annotation it is run on -// one plane with scipy's default structure, 4-connectivity, so two objects -// the model left touching only at a corner stay apart, as it was asked to -// leave them. -// -// The ids: an object the user has already painted part of keeps the id -// painted there most; the rest get fresh ones, the lowest the box does not -// use from 2 up for a uint8 volume, counting up from the box's largest for an -// instance target on a uint16 one (countUp). Ids running out is a refusal -// (AIAnnotateError "refused", 409: the Python raises ValueError and its -// accept route answers 409 with the message; its seed and split routes -// answer 400, as the browser's do), with fill.py's own words. -import { connectedComponents, earlierOffsets, labelComponents } from "../infer/label"; -import { AIAnnotateError, type Labels } from "./types"; - -/** The objects of `mask` (rows × cols, rows first, nonzero = foreground), - * each pixel's id in an int32 array (scipy.ndimage.label's dtype), 0 for - * background, numbered 1..count in raster order of their first pixel as - * scipy numbers them; 4-connected (scipy's default structure in 2D: pixels - * touching only at a corner are different objects). */ -export function label2d(mask: Uint8Array, rows: number, cols: number): { objects: Int32Array; count: number } { - if (mask.length !== rows * cols) throw new Error(`a ${rows}×${cols} mask has ${rows * cols} pixels, not ${mask.length}`); - const { labels, count } = labelComponents(mask, [1, rows, cols], earlierOffsets(2, 1)); - return { objects: Int32Array.from(labels), count }; -} - -/** The largest id the labels' dtype holds (np.iinfo(dtype).max). */ -function biggestId(existing: Labels): number { - return existing instanceof Uint16Array ? 65535 : 255; -} - -function dtypeName(existing: Labels): string { - return existing instanceof Uint16Array ? "uint16" : "uint8"; -} - -/** `count` ids for new objects in a box that holds `existing` (fill._fresh_ids). - * - * `countUp`: from one past the box's largest label, so an affinity target's - * objects stay apart from every object nearby (ids elsewhere in the volume - * are not looked at: affinities only compare nearby voxels). Else the lowest - * ids the box does not use, so a uint8 volume's 254 ids are never run - * through: a repeat in another box is only a repeated colour, which a binary - * or distance target does not see. Throws AIAnnotateError("refused", 409) - * when the dtype has no room. */ -export function freshIds(existing: Labels, count: number, countUp: boolean): number[] { - const biggest = biggestId(existing); - const ids: number[] = []; - if (countUp) { - let max = 0; - for (let i = 0; i < existing.length; i++) if (existing[i] > max) max = existing[i]; - const first = Math.max(max, 1) + 1; - for (let k = 0; k < count; k++) ids.push(first + k); - } else { - const used = new Set(); - for (let i = 0; i < existing.length; i++) if (existing[i] >= 2) used.add(existing[i]); - const last = Math.min(biggest, used.size + count + 2); - for (let id = 2; id <= last && ids.length < count; id++) if (!used.has(id)) ids.push(id); - } - if (ids.length < count || (ids.length && ids[ids.length - 1] > biggest)) { - throw new AIAnnotateError( - "refused", - `${count} new objects do not fit in the volume's ${dtypeName(existing)} labels: make a new annotation volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min size or the threshold so the seed makes fewer objects`, - 409, - ); - } - return ids; -} - -/** Labels for a box from a segmentation: an id per object inside, 1 outside - * (fill.seed_labels). - * - * `objects` is a segmenter's output over the box (0 background, objects - * 1..n; label2d's here), `existing` what the box holds now, the same length. - * Each object gets an id of its own (freshIds; `countUp` for an instance - * target), so neighbouring objects are told apart, and a merge the model - * made shows as one colour to split; but an object the user has already - * painted part of takes the id painted there most (on a tie, the larger id, - * as the Python's stable sort of the pairs leaves it), or the unpainted rest - * of it would be taught as a different object from its painted part. A - * binary or distance target reads every id as foreground, so the ids cost - * it nothing. The result is of `existing`'s dtype. */ -export function seedLabels(objects: ArrayLike, existing: Labels, countUp: boolean): Labels { - const n = existing.length; - if (objects.length !== n) throw new Error(`${objects.length} object labels for a box of ${n} voxels`); - const labels: Labels = existing instanceof Uint16Array ? new Uint16Array(n) : new Uint8Array(n); - let maxObject = 0; - for (let i = 0; i < n; i++) { - labels[i] = objects[i] > 0 ? 2 : 1; - if (objects[i] > maxObject) maxObject = objects[i]; - } - if (maxObject === 0) return labels; - - // For each object, how many of its voxels hold each painted id. - const painted = new Map>(); - const present = new Uint8Array(maxObject + 1); - for (let i = 0; i < n; i++) { - const o = objects[i]; - if (o <= 0) continue; - present[o] = 1; - if (existing[i] < 2) continue; - let counts = painted.get(o); - if (!counts) painted.set(o, (counts = new Map())); - counts.set(existing[i], (counts.get(existing[i]) ?? 0) + 1); - } - const ids = new Array(maxObject + 1).fill(0); - for (const [o, counts] of painted) { - let best = 0, bestCount = -1; - for (const [id, count] of counts) { - if (count > bestCount || (count === bestCount && id > best)) { - best = id; - bestCount = count; - } - } - ids[o] = best; - } - const fresh: number[] = []; - for (let o = 1; o <= maxObject; o++) if (present[o] && ids[o] === 0) fresh.push(o); - const newIds = freshIds(existing, fresh.length, countUp); - fresh.forEach((o, k) => (ids[o] = newIds[k])); - for (let i = 0; i < n; i++) if (objects[i] > 0) labels[i] = ids[objects[i]]; - return labels; -} - -/** What relabelObjects counts (fill.relabel_objects's counts): the - * components found, those given a fresh id (`split`), and those that held - * more than one id (`merged`). */ -export interface RelabelCounts { - objects: number; - split: number; - merged: number; -} - -/** The box's foreground (labels 2 and up) relabelled by connected component - * (fill.relabel_objects): Split Objects, for a merge the model made. - * - * Each component takes the id most of its voxels hold (on a tie, the larger - * id, as seedLabels decides one), so a stroke that joins two objects merges - * them into the id of the bigger. An id left on several components, an - * object a background wall cut in two, stays on the largest (the first in - * component order among equals, numpy's argmax) and the others get fresh - * ids (freshIds, handed out in order of the old id and then of the - * component, as the Python lists them). Background (1) and unpainted (0) - * voxels are left alone. `shape` is the box's (z, y, x); the components are - * infer/label.ts's (scipy.ndimage.label's), `connectivity` and `perSlice` - * as for a seed's: with perSlice each z slice's pieces are components on - * their own, so a wall in one slice splits that slice. A cut without - * perSlice has to go through every slice the object spans, or the two - * sides still touch above or below it, and `split` is 0. Returns the - * labels, of `existing`'s dtype, and the counts. */ -export function relabelObjects( - existing: Labels, - shape: readonly number[], - options: { countUp?: boolean; connectivity: number; perSlice: boolean }, -): { labels: Labels; counts: RelabelCounts } { - const n = existing.length; - if (shape.length !== 3 || shape[0] * shape[1] * shape[2] !== n) throw new Error(`a box of ${shape.join("×")} does not hold ${n} labels`); - const labels = existing.slice(); - const counts: RelabelCounts = { objects: 0, split: 0, merged: 0 }; - const foreground = new Uint8Array(n); - let any = false; - for (let i = 0; i < n; i++) { - if (existing[i] >= 2) { - foreground[i] = 1; - any = true; - } - } - if (!any) return { labels, counts }; - const components = connectedComponents(foreground, shape, { connectivity: options.connectivity, minSize: 0, perSlice: options.perSlice }); - let count = 0; - for (let i = 0; i < n; i++) if (components[i] > count) count = components[i]; - - // For each component, how many of its voxels hold each id, and its size. - const held: Map[] = Array.from({ length: count + 1 }, () => new Map()); - const sizes = new Uint32Array(count + 1); - for (let i = 0; i < n; i++) { - if (!foreground[i]) continue; - const c = components[i]; - sizes[c]++; - held[c].set(existing[i], (held[c].get(existing[i]) ?? 0) + 1); - } - const majority = new Array(count + 1).fill(0); - for (let c = 1; c <= count; c++) { - let best = 0, bestCount = -1; - for (const [id, k] of held[c]) { - if (k > bestCount || (k === bestCount && id > best)) { - best = id; - bestCount = k; - } - } - majority[c] = best; - if (held[c].size > 1) counts.merged++; - } - counts.objects = count; - - // The components that share their id with another: all but the largest - // get a fresh one, by old id ascending, then component ascending - // (np.unique of the ids, np.flatnonzero of each one's components). - const claimants = new Map(); - for (let c = 1; c <= count; c++) { - const list = claimants.get(majority[c]); - if (list) list.push(c); - else claimants.set(majority[c], [c]); - } - const fresh: number[] = []; - for (const id of [...claimants.keys()].sort((a, b) => a - b)) { - const list = claimants.get(id)!; - if (list.length < 2) continue; - let keep = list[0]; - for (const c of list) if (sizes[c] > sizes[keep]) keep = c; - for (const c of list) if (c !== keep) fresh.push(c); - } - const next = majority.slice(); - if (fresh.length) { - const ids = freshIds(existing, fresh.length, options.countUp ?? false); - fresh.forEach((c, k) => (next[c] = ids[k])); - } - counts.split = fresh.length; - for (let i = 0; i < n; i++) if (foreground[i]) labels[i] = next[components[i]]; - return { labels, counts }; -} diff --git a/web/src/ai-annotate/mask.ts b/web/src/ai-annotate/mask.ts index a6e0eaee..9337abc4 100644 --- a/web/src/ai-annotate/mask.ts +++ b/web/src/ai-annotate/mask.ts @@ -1,101 +1,70 @@ -// From an EM plane to the grey image a model is sent, and from its reply to a -// mask: the port of cellmap_flow/ai_annotate/mask_extraction.py (adapted there -// from ask-to-mask) and of staging.py's overlay. Pure functions over typed -// arrays; the canvas is images.ts's business. -import type { RgbaImage } from "./types"; +// The mask in a model's reply and what the review shows of it, in the +// browser: the Python both modes run decides it all +// (cellmap_flow/portable/ai_annotate.py, in the engine's Pyodide worker +// through portable/ai_annotate_calls.py), as the server's backend and +// staging do: the reply resized back to the size sent (Pillow's bilinear +// filter; the worker loads Pillow the first time), the padding cut off and +// the target colour's pixels taken (model_mask); the mask's objects put on +// the write box and the image sent tinted where they are (stage). Here are +// the calls, with the images as the canvas holds them. +import { AI_PYTHON } from "./plane"; +import type { PlanePlan, PythonCall, RgbaImage } from "./types"; -/** `plane` (rows × cols values, rows first) as uint8 grey levels - * (mask_extraction.normalize_to_uint8). Values already 0..255 integers - * (`isUint8`) pass through unchanged; anything else is clipped to its 1st to - * 99th percentile over the plane and stretched to 0-255, as raw EM is not - * always 8-bit and a few saturated pixels should not flatten the contrast. */ -export function normalizeToUint8(plane: ArrayLike, isUint8: boolean): Uint8Array { - const n = plane.length; - const out = new Uint8Array(n); - if (isUint8) { - for (let i = 0; i < n; i++) out[i] = plane[i]; - return out; - } - const sorted = Float64Array.from(plane as ArrayLike).sort(); - let lo = percentile(sorted, 1); - let hi = percentile(sorted, 99); - if (hi <= lo) hi = lo + 1; - const scale = 255 / (hi - lo); - for (let i = 0; i < n; i++) { - const v = Math.min(Math.max(plane[i], lo), hi); - out[i] = Math.trunc((v - lo) * scale); // astype(np.uint8) truncates - } - return out; +/** The mask in `reply`, the image a model sent back (decoded), and the reply + * lined up with the plane (portable/ai_annotate.py model_mask): resized back + * to `sent`, the image it was sent (the plane padded to a square), when it + * came at another size, the padding cut off the right and bottom to the + * plane `image`'s size, and the pixels painted `target` taken. The mask has + * a byte a pixel of the plane, rows first, 1 where painted. */ +export async function modelMask( + python: PythonCall, + reply: RgbaImage, + sent: { width: number; height: number }, + image: { width: number; height: number }, + target: readonly [number, number, number], +): Promise<{ mask: Uint8Array; model_image: RgbaImage }> { + const pixels = new Uint8Array(reply.data).buffer; + const { arrays } = await python<{ value: Record; arrays: ArrayBuffer[] }>(`${AI_PYTHON}:model_mask`, { + shape: [reply.height, reply.width], sent: [sent.height, sent.width], image: [image.height, image.width], target_rgb: target, + }, [pixels]); + return { mask: new Uint8Array(arrays[0]), model_image: { width: image.width, height: image.height, data: new Uint8ClampedArray(arrays[1]) } }; } -/** numpy's default (linear) percentile of sorted values. */ +/** Have the Python import what `stageMask` labels the mask's objects with + * (scipy, which the worker loads the first time a call needs it: seconds, + * once a page), while the model works on the plane, so the answer is not + * kept waiting for it. Never fails: a failure here is the stage's to report. */ +export function prepareMask(python: PythonCall): void { + python(`${AI_PYTHON}:prepare`, {}).catch(() => {}); +} + +/** What the review shows of `mask` (a byte a pixel of the plan's image, rows + * first, nonzero where painted), as the server stages it + * (ai_annotate_calls.py stage): its objects on the write box's in-plane + * voxels, z, y, x order, 0 background and 1..n the objects + * (mask_to_write_shape), the share of the box they mark, and `image`, the + * image sent, tinted `target` where they are (overlay). */ +export async function stageMask( + python: PythonCall, + plan: PlanePlan, + mask: Uint8Array, + image: RgbaImage, + target: readonly [number, number, number], +): Promise<{ objects: Int32Array; maskFraction: number; overlay: RgbaImage }> { + const { value, arrays } = await python<{ value: { mask_fraction: number }; arrays: ArrayBuffer[] }>( + `${AI_PYTHON}:stage`, { plan, target_rgb: target }, [new Uint8Array(mask).buffer, new Uint8Array(image.data).buffer]); + return { + objects: new Int32Array(arrays[0]), + maskFraction: value.mask_fraction, + overlay: { width: image.width, height: image.height, data: new Uint8ClampedArray(arrays[1]) }, + }; +} + +/** numpy's default (linear) percentile of sorted values (the raw's window + * the ranking stretches a model's input to, backend/routes/rank.ts). */ export function percentile(sorted: Float64Array, q: number): number { if (sorted.length === 0) return 0; const rank = (q / 100) * (sorted.length - 1); const lo = Math.floor(rank), hi = Math.ceil(rank); return sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo); } - -/** A grey plane (rows × cols uint8, rows first) as the RGBA image models - * take (mask_extraction.slice_to_rgb): the grey in every channel, alpha 255. */ -export function greyToRgba(grey: Uint8Array, rows: number, cols: number): RgbaImage { - if (grey.length !== rows * cols) throw new Error(`a ${rows}×${cols} plane has ${rows * cols} pixels, not ${grey.length}`); - const data = new Uint8ClampedArray(rows * cols * 4); - for (let i = 0, j = 0; i < grey.length; i++, j += 4) { - data[j] = data[j + 1] = data[j + 2] = grey[i]; - data[j + 3] = 255; - } - return { width: cols, height: rows, data }; -} - -/** The pixels of the model's reply painted `target` (mask_extraction.extract_mask), - * one byte per pixel, rows first, 1 where it is. - * - * Each pixel scores how strongly it shows the target colour: its weakest - * channel among those the target has at full strength (255), minus its - * strongest deviation elsewhere: a channel the target has off (0) being on, - * or a channel the target has at part strength being away from that - * strength. For red (255, 0, 0) that is R − max(G, B): red that dominates. - * A pixel counts when its score reaches `threshold`. The part-strength term - * is what lets orange (255, 128, 0) or purple (128, 0, 255) be found at all. */ -export function extractMask(image: RgbaImage, target: readonly [number, number, number], threshold = 200): Uint8Array { - const full: number[] = [], off: number[] = [], part: number[] = []; - target.forEach((t, c) => (t >= 255 ? full : t <= 0 ? off : part).push(c)); - const { width, height, data } = image; - const mask = new Uint8Array(width * height); - for (let i = 0, j = 0; i < mask.length; i++, j += 4) { - let strength = 255; - for (const c of full) strength = Math.min(strength, data[j + c]); - let deviation = -Infinity; - for (const c of off) deviation = Math.max(deviation, data[j + c]); - for (const c of part) deviation = Math.max(deviation, Math.abs(data[j + c] - target[c])); - const score = deviation === -Infinity ? strength : strength - deviation; - if (score >= threshold) mask[i] = 1; - } - return mask; -} - -/** How strongly the overlay tints the mask with the target colour (staging.py OVERLAY_ALPHA). */ -export const OVERLAY_ALPHA = 0.5; - -/** `image` with the pixels `mask` marks (one byte per pixel of `image`, rows - * first, as the mask of the write box laid out as the image is and scaled to - * it) tinted `rgb` (staging.overlay): the review's third picture, what - * accepting writes, not the model's own image. */ -export function overlay(image: RgbaImage, mask: Uint8Array, rgb: readonly [number, number, number], alpha = OVERLAY_ALPHA): RgbaImage { - if (mask.length !== image.width * image.height) throw new Error(`the mask has ${mask.length} pixels, the image ${image.width * image.height}`); - const data = new Uint8ClampedArray(image.data); - for (let i = 0, j = 0; i < mask.length; i++, j += 4) { - if (!mask[i]) continue; - for (let c = 0; c < 3; c++) data[j + c] = Math.round((1 - alpha) * data[j + c] + alpha * rgb[c]); - } - return { width: image.width, height: image.height, data }; -} - -/** The share of `mask`'s pixels set (staging.py's mask_fraction). */ -export function maskFraction(mask: ArrayLike): number { - if (mask.length === 0) return 0; - let n = 0; - for (let i = 0; i < mask.length; i++) if (mask[i]) n++; - return n / mask.length; -} diff --git a/web/src/ai-annotate/plane.ts b/web/src/ai-annotate/plane.ts index c962d091..11f6aa5d 100644 --- a/web/src/ai-annotate/plane.ts +++ b/web/src/ai-annotate/plane.ts @@ -1,9 +1,12 @@ -// From a planned plane to labels: read the raw, build the request, put the -// mask on the write box, label it. The port of -// cellmap_flow/ai_annotate/pipeline.py (read_plane, _resize, build_request, -// mask_to_write_shape, labels_for_box, raw_extent_nm) for the browser, where -// the raw comes from the engine's reader (Inference.readRaw) over the -// DatasetInfo the engine opened, and PIL's resampling is done by hand. +// Which plane an AI annotation reads, and the image the model is sent, in the +// browser: the raw read here, from the engine's reader (Inference.readRaw) +// over the DatasetInfo the engine opened, and everything decided about it by +// the Python both modes run (cellmap_flow/portable/ai_annotate.py, in the +// engine's Pyodide worker through portable/ai_annotate_calls.py): the plan +// (plan_plane: the field around the point, its write box and its raw box), +// the plane laid out as the viewer's panel draws it, resampled to the image +// and scaled to 8-bit grey. So the plane planned and the image sent are the +// server's for the same volume and point, by construction. // // Where voxel 0 is, on the raw side. A DatasetInfo level's `origin` is the // OME translation as chunkmirage reads it (web/chunkmirage/web/src/ome.ts @@ -15,38 +18,38 @@ // read_plane passes raw_box, holds), how infer/geometry.ts outputLevel // treats it (the served level's origin is the raw corner, `origin - voxel/2`, // plus half an output voxel) and so how finetune/volume.ts planVolume gets a -// volume's dataset_offset_nm, which fill.ts volumeCornerNm reads as a centre. -// So the raw grid rawBox is given here starts at `origin - voxel / 2`, and -// the plane read and the write box cover the same physical area (the test -// in test/unit/ai-annotate-plane.test.ts checks that). viewer/layers.ts -// centrePosition's comment calls a level's origin the corner of its voxel -// 0; it is not, by the code above (the comment only shifts the initial view -// centre by half a voxel, which is why nothing shows it). -// -// The resampling is PIL's (libImaging/Resample.c, Pillow 12: precompute_coeffs -// and the two 32-bit float passes; Geometry.c's ImagingScaleAffine for -// NEAREST), followed operation by operation so the image sent and the mask's -// voxels are the server's: a box filter when the plane shrinks to the image -// (membranes fade rather than alias), bilinear when it grows (the coarse -// axis of an anisotropic plane), each pass accumulated in double and stored -// as float32 as PIL stores its "F" images; nearest neighbour, pixel centre -// to voxel centre, for the mask's objects. -import type { DatasetInfo, Inference, PaintVolume } from "../contracts"; +// volume's dataset_offset_nm, which the Python reads as a centre. So the raw +// grid the plan's raw box is planned on starts at `origin - voxel / 2` +// (levelCornerNm), and the plane read and the write box cover the same +// physical area (test/unit/ai-annotate-python.test.ts checks that). +// viewer/layers.ts centrePosition's comment calls a level's origin the +// corner of its voxel 0; it is not, by the code above (the comment only +// shifts the initial view centre by half a voxel, which is why nothing +// shows it). import { padded } from "../../chunkmirage/web/src/stages"; +import type { DatasetInfo, Inference, PaintVolume } from "../contracts"; import { chooseRawLevel, clipBox, type Level } from "../infer/geometry"; -import { clickPx, fromScreen, type Plane, planeAxes, rawBox, screenShape, toScreen, writeShape } from "./geometry"; -import { label2d, seedLabels } from "./labels"; -import { greyToRgba, normalizeToUint8 } from "./mask"; import { buildRecolorPrompt } from "./prompts"; -import type { Labels, OrganelleProfile, PlanePlan, SegmentRequest } from "./types"; +import { AIAnnotateError, type DepthAxis, type OrganelleProfile, type PlanePlan, type RgbaImage, type SegmentRequest } from "./types"; + +/** The AI annotation's entry points in the portable Python, by module path. */ +export const AI_PYTHON = "cellmap_flow.portable.ai_annotate_calls"; -/** A raw plane as readPlane returns it: `plan.image_shape` rows × cols, laid - * out as on screen, float32 (the dataset's values; a float64 dataset loses - * precision as it does through PIL); `isUint8` when the dataset is uint8 - * and the plane was not resized, so normalizeToUint8 passes the values - * through as the Python's does with a uint8 plane. */ -export interface RawPlane extends Plane { - isUint8: boolean; +/** What the Python answers for a call the user is to be told no to: a point + * outside the volume or the data, ids running out (ai_annotate_calls.py). */ +export interface PythonRefusal { + refused: string; +} + +/** A plane planned, as the Python's plan call answers it: the plan + * (PlanePlan.to_dict), the point's pixel in the image, the write box's + * in-plane shape, and the raw box read on the level chosen (its index here). */ +export interface PlannedPlane { + plan: PlanePlan; + click_px: [number, number]; + write_shape: [number, number]; + raw_box: { begin: number[]; count: number[] }; + level: number; } /** The lower corner of a raw level's voxel 0, in nm: its origin, the OME @@ -60,7 +63,7 @@ export function levelExtentNm(level: Pick): } /** `[lo, hi]`, world nm z, y, x: where the volume's raw data is, read at - * its input voxel size (pipeline.raw_extent_nm: planPlane's `dataBoxNm`). + * its input voxel size (pipeline.raw_extent_nm: plan_plane's `data_box_nm`). * The level is the one chooseRawLevel picks for `volume.input_voxel_size`, * which planVolume set to a level's own voxel size. */ export function rawExtentNm(dataset: Pick, volume: Pick): [number[], number[]] { @@ -68,266 +71,66 @@ export function rawExtentNm(dataset: Pick, volume: Pick

o + size[a])]; } -/** The raw plane `plan` reads, as `plan.image_shape` laid out as on screen - * (pipeline.read_plane). - * - * Read as stored (no input normalization: the image is scaled to 8 bits for - * the model on its own) from the level of `dataset` at the volume's input - * voxel size (chooseRawLevel), over the box rawBox plans on that level's - * grid, clipped to the level and zero past it (chunkmirage's `padded`, as - * finetune/samples.ts pads a training block); the one plane along the depth - * axis is taken, laid out as the panel draws it (toScreen) and resampled to - * the plan's image (resizePlane). */ -export async function readPlane(infer: Pick, dataset: DatasetInfo, volume: Pick, plan: PlanePlan): Promise { - if (dataset.channels > 1) throw new Error(`${dataset.url} has ${dataset.channels} channels; the browser mode annotates single-channel volumes`); - const levelIndex = chooseRawLevel(dataset.levels, volume.input_voxel_size); - const level = dataset.levels[levelIndex]; - const { begin, count } = rawBox(plan, level.voxel, levelCornerNm(level)); - const end = begin.map((b, a) => b + count[a]); - const inside = clipBox(begin, count, level.shape); - const data = inside ? await infer.readRaw(levelIndex, inside.lo, inside.hi) : null; - const block = padded(data, dataset.dtype, 1, inside?.lo ?? begin, inside?.hi ?? begin, begin, end, "zero"); - - // The first plane along the depth axis (np.take(data, 0, axis)), in z, y, x order. - const [rowAxis, colAxis] = planeAxes(plan); - const rows = count[rowAxis], cols = count[colAxis]; - const plane = new Float32Array(rows * cols); - const stride = [count[1] * count[2], count[2], 1]; - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const value = block.data[r * stride[rowAxis] + c * stride[colAxis]]; - plane[r * cols + c] = Number(value); - } - } - const onScreen = toScreen(plane, rows, cols, plan.depth_axis, plan.display_dimensions); - const [imageRows, imageCols] = plan.image_shape; - const resized = onScreen.rows !== imageRows || onScreen.cols !== imageCols; - const image = resized ? resizePlane(onScreen, imageRows, imageCols) : onScreen; - return { ...image, isUint8: dataset.dtype === "uint8" && !resized }; -} - -// --------------------------------------------------------------------------- -// PIL's resampling (pipeline._resize and mask_to_write_shape's NEAREST) -// --------------------------------------------------------------------------- - -interface Filter { - support: number; - weight(x: number): number; +/** The plane through `pointNm` (world nm, z, y, x) across `depthAxis`, planned + * by the Python (portable/ai_annotate.py plan_plane, whose docstring says + * what the plane is): over `volume`'s grid (its dataset_shape_voxels), the + * field `cropSizePx` annotation voxels a side, clipped to the raw data's + * extent (rawExtentNm), laid out for the viewer's `displayDimensions` (z, y, + * x when null), with its raw box on the level of `dataset` at the volume's + * input voxel size (chooseRawLevel). Throws AIAnnotateError("refused", 400) + * when the point is outside the volume or the data. */ +export async function planPlane( + infer: Pick, + dataset: DatasetInfo, + volume: Pick, + pointNm: readonly number[], + depthAxis: DepthAxis, + cropSizePx: number, + displayDimensions: readonly string[] | null = null, +): Promise { + const level = chooseRawLevel(dataset.levels, volume.input_voxel_size); + const raw = dataset.levels[level]; + const answer = await infer.python | PythonRefusal>(`${AI_PYTHON}:plan`, { + volume: { output_voxel_size: volume.output_voxel_size, input_voxel_size: volume.input_voxel_size, dataset_offset_nm: volume.dataset_offset_nm }, + volume_shape: volume.dataset_shape_voxels, point_nm: pointNm, depth_axis: depthAxis, crop_size_px: cropSizePx, + data_box_nm: rawExtentNm(dataset, volume), display_dimensions: displayDimensions, + raw: { voxel: raw.voxel, corner: levelCornerNm(raw) }, + }); + if ("refused" in answer) throw new AIAnnotateError("refused", answer.refused, 400); + return { ...answer, level }; } -/** PIL's box filter: Image.BOX. */ -const BOX: Filter = { support: 0.5, weight: (x) => (x > -0.5 && x <= 0.5 ? 1 : 0) }; -/** PIL's triangle filter: Image.BILINEAR. */ -const BILINEAR: Filter = { - support: 1, - weight: (x) => { - x = Math.abs(x); - return x < 1 ? 1 - x : 0; - }, -}; - -/** PIL's precompute_coeffs for one axis of `inSize` samples resampled to - * `outSize`: for each output sample, the first input sample it reads - * (`bounds[2i]`), how many (`bounds[2i + 1]`) and their normalized weights - * (`ksize` per output sample). The filter's support is scaled by the - * shrink factor when shrinking, so every input sample is weighed. */ -function coefficients(inSize: number, outSize: number, filter: Filter): { bounds: Int32Array; weights: Float64Array; ksize: number } { - const scale = inSize / outSize; - const filterscale = scale < 1 ? 1 : scale; - const support = filter.support * filterscale; - const ksize = Math.ceil(support) * 2 + 1; - const bounds = new Int32Array(outSize * 2), weights = new Float64Array(outSize * ksize); - for (let xx = 0; xx < outSize; xx++) { - const center = (xx + 0.5) * scale; - const ss = 1 / filterscale; - let xmin = Math.trunc(center - support + 0.5); // "Round the value": C's (int) truncates - if (xmin < 0) xmin = 0; - let xmax = Math.trunc(center + support + 0.5); - if (xmax > inSize) xmax = inSize; - xmax -= xmin; - let ww = 0; - const k = xx * ksize; - for (let x = 0; x < xmax; x++) { - const w = filter.weight((x + xmin - center + 0.5) * ss); - weights[k + x] = w; - ww += w; - } - if (ww !== 0) for (let x = 0; x < xmax; x++) weights[k + x] /= ww; - bounds[xx * 2] = xmin; - bounds[xx * 2 + 1] = xmax; - } - return { bounds, weights, ksize }; -} - -/** `plane` (float32 values as PIL's "F" image holds them) resampled to - * `rows × cols` (pipeline._resize): itself when it already is that. Shrinking - * (neither side grows) averages each pixel's footprint (PIL's box filter), so - * fine membranes fade rather than alias; otherwise (the coarse axis of an - * anisotropic plane, or a mixed case) interpolates linearly, PIL's BILINEAR. - * The horizontal pass runs first, the vertical over its float32 result, as - * PIL's ImagingResampleInner does, each sum accumulated in double. */ -export function resizePlane(plane: Plane, rows: number, cols: number): Plane { - if (plane.rows === rows && plane.cols === cols) return plane; - if (rows < 1 || cols < 1) throw new Error(`cannot resize a plane to ${rows}×${cols}`); - const filter = plane.rows >= rows && plane.cols >= cols ? BOX : BILINEAR; - let data = plane.data, inRows = plane.rows, inCols = plane.cols; - if (inCols !== cols) { - const { bounds, weights, ksize } = coefficients(inCols, cols, filter); - const out = new Float32Array(inRows * cols); - for (let yy = 0; yy < inRows; yy++) { - for (let xx = 0; xx < cols; xx++) { - const xmin = bounds[xx * 2], xmax = bounds[xx * 2 + 1], k = xx * ksize; - let ss = 0; - for (let x = 0; x < xmax; x++) ss += data[yy * inCols + x + xmin] * weights[k + x]; - out[yy * cols + xx] = ss; // stored as float32, as PIL's "F" image is - } - } - data = out; - inCols = cols; - } - if (inRows !== rows) { - const { bounds, weights, ksize } = coefficients(inRows, rows, filter); - const out = new Float32Array(rows * inCols); - for (let yy = 0; yy < rows; yy++) { - const ymin = bounds[yy * 2], ymax = bounds[yy * 2 + 1], k = yy * ksize; - for (let xx = 0; xx < inCols; xx++) { - let ss = 0; - for (let y = 0; y < ymax; y++) ss += data[(y + ymin) * inCols + xx] * weights[k + y]; - out[yy * inCols + xx] = ss; - } - } - data = out; - inRows = rows; - } - return { rows, cols, data }; -} - -/** The input index each of `outSize` output samples takes from `inSize` - * inputs under PIL's NEAREST (Geometry.c ImagingScaleAffine): the input - * sample under the output sample's centre, the position accumulated in - * double as PIL accumulates it, so a centre that lands within rounding of a - * boundary takes the side PIL takes. */ -function nearestIndices(inSize: number, outSize: number): Int32Array { - const step = inSize / outSize; - const indices = new Int32Array(outSize); - let position = step * 0.5; - for (let i = 0; i < outSize; i++) { - indices[i] = Math.min(Math.max(Math.trunc(position), 0), inSize - 1); - position += step; - } - return indices; +/** The image sent for `planned`'s plane (pipeline.read_plane and + * build_request's image): its raw box read as stored (no input + * normalization: the image is scaled to 8 bits for the model on its own) + * from its level, clipped to the level and zero past it (chunkmirage's + * `padded`, as finetune/samples.ts pads a training block), and handed to the + * Python, which takes the plane along the depth axis, lays it out as the + * panel draws it, resamples it to the plan's image and scales it to grey. + * RGBA, the grey in every channel. */ +export async function readImage(infer: Pick, dataset: DatasetInfo, planned: PlannedPlane): Promise { + if (dataset.channels > 1) throw new Error(`${dataset.url} has ${dataset.channels} channels; the browser mode annotates single-channel volumes`); + const { begin, count } = planned.raw_box; + const end = begin.map((b, a) => b + count[a]); + const inside = clipBox(begin, count, dataset.levels[planned.level].shape); + const data = inside ? await infer.readRaw(planned.level, inside.lo, inside.hi) : null; + const block = padded(data, dataset.dtype, 1, inside?.lo ?? begin, inside?.hi ?? begin, begin, end, "zero").data; + const bytes = block.buffer.slice(block.byteOffset, block.byteOffset + block.byteLength) as ArrayBuffer; + const rgba = await infer.python(`${AI_PYTHON}:request_image`, { plan: planned.plan, dtype: dataset.dtype, count }, [bytes]); + const [rows, cols] = planned.plan.image_shape; + return { width: cols, height: rows, data: new Uint8ClampedArray(rgba) }; } -// --------------------------------------------------------------------------- -// The request and the mask -// --------------------------------------------------------------------------- - -/** The SegmentRequest for `plane` (pipeline.build_request): its grey RGBA - * image (normalizeToUint8, greyToRgba), the prompt and the colour. The - * prompt states `plan.resolution_nm`, the pixel size of the image actually - * sent; `promptOverride` replaces the catalog's description - * (prompts.buildRecolorPrompt). A plane that is not the plan's image is an - * Error (the Python's ValueError). */ -export function buildRequest(plane: RawPlane, plan: PlanePlan, profile: OrganelleProfile, promptOverride: string | null = null): SegmentRequest { - const [rows, cols] = plan.image_shape; - if (plane.rows !== rows || plane.cols !== cols) throw new Error(`The plane is (${plane.rows}, ${plane.cols}), the plan's image (${rows}, ${cols})`); - const image = greyToRgba(normalizeToUint8(plane.data, plane.isUint8), rows, cols); +/** The SegmentRequest for `image` (pipeline.build_request): the image, the + * prompt, the colour and the point's pixel. The prompt states + * `plan.resolution_nm`, the pixel size of the image actually sent; + * `promptOverride` replaces the catalog's description + * (prompts.buildRecolorPrompt). */ +export function buildRequest(image: RgbaImage, planned: PlannedPlane, profile: OrganelleProfile, promptOverride: string | null = null): SegmentRequest { return { image, - prompt: buildRecolorPrompt(profile, plan.resolution_nm, promptOverride), + prompt: buildRecolorPrompt(profile, planned.plan.resolution_nm, promptOverride), target_rgb: [profile.rgb[0], profile.rgb[1], profile.rgb[2]], - click_px: clickPx(plan), + click_px: planned.click_px, }; } - -/** The objects of `mask` (one byte per pixel of the plan's image, rows - * first, as on screen, nonzero = set) on the write box's in-plane voxels, in - * z, y, x order (writeShape; pipeline.mask_to_write_shape): 0 for - * background and 1..n for the objects, numbered in the box's raster order - * as label2d numbers a mask. - * - * Objects are the mask's 4-connected pieces (label2d, so pieces touching - * only at a corner stay apart, as the model was asked to leave them), found - * at the image's own resolution, where the model drew the lines between - * them: on voxels coarser than the pixels a line can fall between the - * pixels the voxels take, and the objects either side of it still keep ids - * of their own. Then nearest neighbour, pixel centre to voxel centre: each - * annotation voxel takes the object of the pixel at its centre, so the mask - * keeps hard edges and lands where it was drawn whatever the two sizes - * are. An object no voxel centre falls in is lost. */ -export function maskToWriteShape(mask: Uint8Array, plan: PlanePlan): Int32Array { - const [imageRows, imageCols] = plan.image_shape; - if (mask.length !== imageRows * imageCols) throw new Error(`the mask has ${mask.length} pixels, the plan's image ${imageRows}×${imageCols}`); - const { objects, count } = label2d(mask, imageRows, imageCols); - const [rows, cols] = screenShape(writeShape(plan), plan.depth_axis, plan.display_dimensions); - let onScreen = objects; - if (rows !== imageRows || cols !== imageCols) { - const rowOf = nearestIndices(imageRows, rows), colOf = nearestIndices(imageCols, cols); - onScreen = new Int32Array(rows * cols); - for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) onScreen[r * cols + c] = objects[rowOf[r] * imageCols + colOf[c]]; - } - const write = fromScreen(onScreen, rows, cols, plan.depth_axis, plan.display_dimensions).data; - // Renumbered by first voxel, so fresh ids go to the objects in the order - // labelling the box itself would give them, whatever the screen layout. - const number = new Int32Array(count + 1); - let next = 0; - for (let i = 0; i < write.length; i++) { - const o = write[i]; - if (o === 0) continue; - if (number[o] === 0) number[o] = ++next; - write[i] = number[o]; - } - return write; -} - -/** The labels for the write box [writeLo, writeHi): an id per object of - * `objects`, 1 elsewhere (pipeline.labels_for_box). - * - * `objects` is the in-plane objects (maskToWriteShape), 0 for background, - * `existing` the one-voxel-thick box as the volume holds it (fill.readBox, - * C order), which for a box one voxel thick along `depthAxis` is laid out - * exactly as the plane is. The objects are labelled by seedLabels: an - * object the user has already painted part of keeps that id, the rest get - * fresh ones, counting up for an instance target's `countUp`. - * - * `beside` is the planes on either side of the box along the depth axis, as - * the volume holds them (each laid out like `existing`, or null at the - * volume's edge; the accept route reads them with fill.readBox). Planes are - * annotated one at a time, so an object usually continues one in the plane - * next to it: where this plane is unpainted, the ids beside it count as - * painted here (the first plane given first), and an object takes the id it - * overlaps most, rather than a fresh one that an instance target would read - * as a boundary between the two planes. Fresh ids skip those planes' ids - * too, so a new object does not take the id of a different object right - * next to it, which an instance target would read as one object. */ -export function labelsForBox( - objects: ArrayLike, - existing: Labels, - writeLo: readonly number[], - writeHi: readonly number[], - depthAxis: PlanePlan["depth_axis"], - countUp: boolean, - beside: readonly (Labels | null)[] = [], -): Labels { - const [rowAxis, colAxis] = planeAxes({ depth_axis: depthAxis }); - const rows = writeHi[rowAxis] - writeLo[rowAxis], cols = writeHi[colAxis] - writeLo[colAxis]; - if (writeHi[depthAxis] - writeLo[depthAxis] !== 1) throw new Error(`The box ${writeLo.join(",")}..${writeHi.join(",")} is not one voxel thick along axis ${depthAxis}`); - if (objects.length !== rows * cols) throw new Error(`The mask makes a box of ${rows * cols} voxels; the box holds ${objects.length}`); - if (existing.length !== rows * cols) throw new Error(`The mask makes a box of ${rows * cols} voxels; the box holds ${existing.length}`); - const planes = beside.filter((p): p is Labels => p !== null); - if (planes.some((p) => p.length !== existing.length)) throw new Error(`The planes beside the box must hold ${existing.length} voxels`); - if (!planes.length) return seedLabels(objects, existing, countUp); - - const n = existing.length; - const seen = existing.slice(); - for (const plane of planes) for (let i = 0; i < n; i++) if (seen[i] === 0 && plane[i] >= 2) seen[i] = plane[i]; - // The planes beside are stacked after the box with no objects in them: - // seedLabels labels only the box's objects, but its fresh ids see every - // id in the stack. - const stackExisting: Labels = existing instanceof Uint16Array ? new Uint16Array(n * (1 + planes.length)) : new Uint8Array(n * (1 + planes.length)); - (stackExisting as Uint16Array).set(seen as Uint16Array, 0); - planes.forEach((plane, k) => (stackExisting as Uint16Array).set(plane as Uint16Array, n * (k + 1))); - const stackObjects = new Int32Array(n * (1 + planes.length)); - stackObjects.set(objects, 0); - return seedLabels(stackObjects, stackExisting, countUp).slice(0, n); -} diff --git a/web/src/ai-annotate/types.ts b/web/src/ai-annotate/types.ts index 3fa430ed..b35b1911 100644 --- a/web/src/ai-annotate/types.ts +++ b/web/src/ai-annotate/types.ts @@ -8,15 +8,20 @@ // "AI-assisted annotation in the browser". // // Axes are z, y, x throughout, as in the Python. An image is laid out as the -// viewer's panel draws the plane (geometry.ts toScreen): rows down, columns -// across. +// viewer's panel draws the plane (portable/ai_annotate.py to_screen): rows +// down, columns across. // -// Who implements what: geometry.ts, plane.ts, labels.ts, fill.ts, prompts.ts, -// organelles.ts and mask.ts are pure (no DOM); images.ts is the canvas -// ImageCodec; gemini.ts, fake.ts and providers.ts the Providers; +// Who implements what: the plane, the image sent, the mask and the labels an +// accept writes are the Python both modes run (cellmap_flow/portable/ +// ai_annotate.py), which plane.ts and mask.ts call in the engine's worker; +// fill.ts is the paint store's box reads and writes (the box decisions, +// an accept's and the Label it tools', are portable/fill.py's), prompts.ts +// and organelles.ts the prompt and the catalog (no DOM); images.ts is the +// canvas ImageCodec; gemini.ts, fake.ts and providers.ts the Providers; // backend/ai-annotate-job.ts and backend/routes/ai-annotate.ts the routes. // The constants with a Python twin are the Python's, exported // (generated/python-data.ts). +import type { Inference } from "../contracts"; import { PYTHON } from "../generated/python-data"; const AI = PYTHON.ai_annotate; @@ -38,8 +43,8 @@ export const PLANE_NAMES: readonly ["XY", "XZ", "YZ"] = ["XY", "XZ", "YZ"]; * pixel size (square pixels); `write_lo`/`write_hi` the annotation voxels * written, [lo, hi), one voxel thick along `depth_axis`; * `display_dimensions` the viewer's, which say how its panel draws the - * plane (geometry.ts screenAxes), absent (z, y, x) in a plan saved before - * plans kept them. */ + * plane (portable/ai_annotate.py screen_axes), absent (z, y, x) in a plan + * saved before plans kept them. */ export interface PlanePlan { depth_axis: DepthAxis; point_nm: number[]; @@ -115,10 +120,12 @@ export const MAX_IMAGE_PIXELS = AI.max_image_pixels; /** The formats a model's image may be in. */ export const ALLOWED_IMAGE_TYPES: readonly string[] = ["image/png", "image/jpeg", "image/webp"]; -/** Encoding, decoding and resizing images: the page's canvas (images.ts), a +/** Encoding and decoding images: the page's canvas (images.ts), a * plain-JavaScript stand-in in tests. A model's image is untrusted input, so * `decode` checks the type and the pixel count before decoding and raises - * AIAnnotateError("bad_response") for anything else. */ + * AIAnnotateError("bad_response") for anything else. Resizing is the + * Python's (mask.ts modelMask), so a reply at another size is lined up with + * the plane as the server lines it up. */ export interface ImageCodec { /** `image` as PNG bytes. */ encodePng(image: RgbaImage): Promise; @@ -126,8 +133,6 @@ export interface ImageCodec { * `mimeType` is what the model declared, which may be empty or wrong: the * bytes decide. */ decode(bytes: Uint8Array, mimeType: string): Promise; - /** `image` at `width` × `height`: bilinear when `smooth`, else nearest neighbour. */ - resize(image: RgbaImage, width: number, height: number, smooth: boolean): Promise; } // --------------------------------------------------------------------------- @@ -202,12 +207,17 @@ export interface ProviderInfo { credentials: CredentialField[]; } +/** The engine's call into the portable Python (Inference.python). */ +export type PythonCall = Inference["python"]; + export interface Provider { readonly info: ProviderInfo; /** Ask `model` to paint the request's structure, with `credentials`; * AIAnnotateError on failure, with a message written by the provider - * module, never the service's own text. */ - segment(request: SegmentRequest, model: string, credentials: Credentials, options?: { signal?: AbortSignal }): Promise; + * module, never the service's own text. `options.python` is the engine's + * Python, which a provider whose model answers with an image takes the + * mask out of it with (mask.ts modelMask); nothing else is handed to it. */ + segment(request: SegmentRequest, model: string, credentials: Credentials, options?: { signal?: AbortSignal; python?: PythonCall }): Promise; /** The image models `credentials` can reach, for a provider that can list * them (the Gemini API lists its models); null when it cannot say. */ listModels?(credentials: Credentials, options?: { signal?: AbortSignal }): Promise; diff --git a/web/src/backend/ai-annotate-job.ts b/web/src/backend/ai-annotate-job.ts index c98b56d4..2576ced1 100644 --- a/web/src/backend/ai-annotate-job.ts +++ b/web/src/backend/ai-annotate-job.ts @@ -17,16 +17,20 @@ // built once when the result is ready and answered with `?preview=1`. // // What the job needs of the rest of the feature comes from the ai-annotate -// modules: the plane's plan and read (geometry.ts, plane.ts), the mask and -// the labels (mask.ts, plane.ts), the write and its undo (fill.ts), the -// prompt (prompts.ts), the audit log (audit.ts) and the day's count -// (usage.ts). The provider and the image codec are the routes' deps. +// modules: the plane's plan and read (plane.ts), the mask and what the review +// shows of it (mask.ts), the box's reads and writes and its undo (fill.ts), +// the prompt (prompts.ts), the audit log (audit.ts) and the day's count +// (usage.ts). Everything those decide about the plane, the image, the mask +// and the labels is the Python both modes run +// (cellmap_flow/portable/ai_annotate.py, in the engine's Pyodide worker +// through Inference.python), so a plane is planned, sent, masked and +// written as the server would. The provider and the image codec are the +// routes' deps. import { record, type AuditEvent } from "../ai-annotate/audit"; -import { paintBox, readBox } from "../ai-annotate/fill"; -import { planPlane, screenShape, toScreen, writeShape } from "../ai-annotate/geometry"; +import { pushUndo, readBox, writeBox } from "../ai-annotate/fill"; import { bytesToBase64 as base64 } from "../ai-annotate/gemini"; -import { maskFraction, overlay } from "../ai-annotate/mask"; -import { buildRequest, labelsForBox, maskToWriteShape, rawExtentNm, readPlane } from "../ai-annotate/plane"; +import { prepareMask, stageMask } from "../ai-annotate/mask"; +import { AI_PYTHON, buildRequest, type PlannedPlane, planPlane, type PythonRefusal, readImage } from "../ai-annotate/plane"; import { buildRecolorPrompt, editablePrompt } from "../ai-annotate/prompts"; import type { Credentials, @@ -38,6 +42,7 @@ import type { PlanePlan, Preview, Provider, + RgbaImage, SegmentRequest, SegmentResult, Stage, @@ -142,7 +147,8 @@ export class AiAnnotateJob { plan: PlanePlan | null = null; request: SegmentRequest | null = null; result: SegmentResult | null = null; - /** The mask's objects on the write box's in-plane voxels (plane.ts maskToWriteShape). */ + /** The mask's objects on the write box's in-plane voxels, z, y, x order + * (portable/ai_annotate.py mask_to_write_shape, through mask.ts stageMask). */ maskWrite: Int32Array | null = null; private current: Promise = Promise.resolve(); @@ -235,11 +241,12 @@ export class AiAnnotateJob { this.setStage("fetching_crop"); const dataset = ctx.infer.dataset(); if (!dataset) throw new Refused("No dataset is open in the viewer.", 400); - const plan = planPlane(args.volume, args.volume.dataset_shape_voxels, args.pointNm, args.depthAxis, this.deps.cropSizePx, rawExtentNm(dataset, args.volume), args.displayDimensions ?? null); + const planned: PlannedPlane = await planPlane(ctx.infer, dataset, args.volume, args.pointNm, args.depthAxis, this.deps.cropSizePx, args.displayDimensions ?? null); + const plan = planned.plan; this.plan = plan; this.writeBox = { lo: plan.write_lo.map((v) => Math.trunc(v)), hi: plan.write_hi.map((v) => Math.trunc(v)) }; - const plane = await readPlane(ctx.infer, dataset, args.volume, plan); - const request: SegmentRequest = buildRequest(plane, plan, args.profile, args.override); + const image = await readImage(ctx.infer, dataset, planned); + const request: SegmentRequest = buildRequest(image, planned, args.profile, args.override); this.request = request; await this.askAndStage(request); } catch (e) { @@ -285,28 +292,32 @@ export class AiAnnotateJob { * reached the model. */ private async askAndStage(request: SegmentRequest): Promise { const args = this.args!; - const { usage, codec } = this.deps; + const { usage, codec, ctx } = this.deps; + const python = ctx.infer.python.bind(ctx.infer); this.setStage("sending"); usage.checkAndCount(this.deps.dailyCallLimit); + // What the mask is labelled with loads while the model works (scipy, the first time). + prepareMask(python); let result: SegmentResult; try { // No outer timeout: the provider times each attempt out itself // (CALL_TIMEOUT_MS) and retries a busy service, so one call may take // several attempts' worth; a signal here would cut the retries short. + // The provider takes the mask out of the model's reply with the + // Python (mask.ts modelMask), as the server's backend does. const credentials = this.deps.credentials(args.providerId) ?? {}; - result = await args.provider.segment(request, args.model, credentials); + result = await args.provider.segment(request, args.model, credentials, { python }); } catch (e) { if (e instanceof AIAnnotateError && NOT_SENT.has(e.category)) usage.refund(); throw e; } this.setStage("extracting_mask"); - const plan = this.plan!; - const maskWrite = maskToWriteShape(result.mask, plan); + const staged = await stageMask(python, this.plan!, result.mask, request.image, request.target_rgb); this.setStage("staging_preview"); - const preview = await buildPreview(codec, request, result, maskWrite, plan); - const fraction = maskFraction(maskWrite); + const preview = await buildPreview(codec, request, result, staged.overlay); + const fraction = staged.maskFraction; this.result = result; - this.maskWrite = maskWrite; + this.maskWrite = staged.objects; this.maskFraction = fraction; this.preview = preview; this.error = null; @@ -367,13 +378,18 @@ export class AiAnnotateJob { * `overwrite` only unannotated voxels are written, as a seed writes them; * with it the staged labels replace what the plane held, except that the * model's background only fills unlabelled voxels: an object already - * painted that the model missed is kept, not erased. `undo` is the - * volume's stack, which gets the box as it was and as it is (fill.ts pushUndo - * keeps UNDO_DEPTH). The route sets the status around the call and answers can_undo. + * painted that the model missed is kept, not erased. Which voxels get + * which ids is the Python's (portable/ai_annotate_calls.py accept: + * accept_labels, as the server's accept route labels the box, then + * fill.paint's write rule), handed the box and the planes beside it as the + * paint store holds them now; the box it answers is written here, and + * `undo`, the volume's stack, gets the box as it was and as it is (fill.ts + * pushUndo keeps UNDO_DEPTH). The route sets the status around the call + * and answers can_undo. * - * Throws a Refused when the volume is gone, and whatever the labelling or - * the write throws (the Python answers a ValueError's message as a 409: - * fill's ids running out, a mask of the wrong shape). */ + * Throws a Refused when the volume is gone, an AIAnnotateError + * ("refused", 409) when the ids run out (the Python's words, as the + * server's accept answers them), and whatever the store's write throws. */ async accept(overwrite: boolean, undo: UndoEntry[]): Promise { const args = this.args!; const plan = this.plan!; @@ -381,26 +397,29 @@ export class AiAnnotateJob { const { ctx } = this.deps; const volume = await readVolume(ctx.paint, args.volume.id); if (!volume) throw new Refused(`The annotation volume ${args.volume.id} is no longer open.`); - // As a seed: an instance target on a uint16 volume counts its ids up, a - // uint8 volume reuses free ones. The server asks what the model trains - // on (INSTANCE_TARGETS: affinities, flows); here the channel-name test - // stands for it, as annotationDtypeFor's does. + // An instance target counts its ids up on a volume of more than 8-bit + // ids (portable/fill.py counts_up). The server asks what the model + // trains on (affinities or Cellpose's flows); here the channel-name test + // stands for it, as the server's annotation_dtype rule does (portable/volume.py): the browser runs no flow model. const entry = (await ctx.catalog.resolve(volume.model_id)) ?? (await ctx.catalog.resolve(volume.model_name)); const instanceTarget = entry ? hasAffinityChannels(entry) : false; - const countUp = instanceTarget && volume.dtype === "uint16"; const lo = plan.write_lo.map((v) => Math.trunc(v)); const hi = plan.write_hi.map((v) => Math.trunc(v)); // Read just before the write, so a stroke painted in them since the plane was staged counts. + const existing = await readBox(ctx.paint, volume, lo, hi); const beside = await planesBeside(ctx.paint, volume, lo, hi, plan.depth_axis); - const labelsFor = (existing: Labels): Labels => { - const labels = labelsForBox(maskWrite, existing, lo, hi, plan.depth_axis, countUp, beside); - if (overwrite) { - for (let i = 0; i < labels.length; i++) if (labels[i] === 1 && existing[i] !== 0) labels[i] = 0; - } - return labels; - }; - const counts = await paintBox(ctx.paint, volume, lo, hi, labelsFor, { undo, overwrite }); + const answer = await ctx.infer.python<{ value: AcceptCounts; arrays: ArrayBuffer[] } | PythonRefusal>(`${AI_PYTHON}:accept`, { + depth_axis: plan.depth_axis, shape: hi.map((h, a) => h - lo[a]), dtype: volume.dtype, instance_target: instanceTarget, + overwrite, beside: beside.map((p) => p !== null), + }, [bytesOf(maskWrite), bytesOf(existing), ...beside.flatMap((p) => (p ? [bytesOf(p)] : []))]); + if ("refused" in answer) throw new AIAnnotateError("refused", answer.refused, 409); + const counts = answer.value; + if (answer.arrays.length) { + const after = volume.dtype === "uint16" ? new Uint16Array(answer.arrays[0]) : new Uint8Array(answer.arrays[0]); + await writeBox(ctx.paint, volume, lo, hi, after); + pushUndo(undo, { lo, hi, before: existing, after: after.slice() }); + } const changed = Boolean(counts.n_foreground || counts.n_background || counts.n_overwritten); const layerRefreshed = changed ? ctx.viewer.refreshLayer(paintLayerName(volume.id)) : false; return { @@ -413,13 +432,27 @@ export class AiAnnotateJob { } } +/** What accepting wrote, as the Python answers it (fill.paint's counts): the + * voxels written with foreground and with background, and those of them + * that held a label before. */ +interface AcceptCounts { + n_foreground: number; + n_background: number; + n_overwritten: number; +} + +/** A typed array's own bytes, copied: a call's arrays are handed to the worker. */ +function bytesOf(a: ArrayBufferView): ArrayBuffer { + return a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength) as ArrayBuffer; +} + // --------------------------------------------------------------------------- // The plane's surroundings // --------------------------------------------------------------------------- /** The planes on either side of the box [lo, hi) along `depthAxis`, as the - * volume holds them (ai_annotate.py _planes_beside), for labelsForBox; null - * for a side past the volume's edge. */ + * volume holds them (ai_annotate.py _planes_beside), for the Python's + * accept_labels; null for a side past the volume's edge. */ export async function planesBeside(store: PaintStore, volume: PaintVolume, lo: number[], hi: number[], depthAxis: DepthAxis): Promise<(Labels | null)[]> { const planes: (Labels | null)[] = []; for (const step of [-1, 1]) { @@ -437,40 +470,20 @@ export async function planesBeside(store: PaintStore, volume: PaintVolume, lo: n } // --------------------------------------------------------------------------- -// The preview (staging.py preview and overlay) +// The preview (staging.py preview) // --------------------------------------------------------------------------- /** The review's images, base64 PNG: the image sent, the model's image (null - * when it drew none) and the input with the voxels the mask labels tinted - * the target colour. The mask is the write box's, laid out as the image is - * (toScreen) and scaled to it by nearest neighbour, so the overlay shows - * what accepting writes, not the model's own image. Every image is encoded - * here, so nothing the model sent reaches the page as it came. */ -export async function buildPreview(codec: ImageCodec, request: SegmentRequest, result: SegmentResult, maskWrite: Int32Array, plan: PlanePlan): Promise { - const [a, b] = writeShape(plan); - const [rows, cols] = screenShape([a, b], plan.depth_axis, plan.display_dimensions); - const screen = toScreen(Uint8Array.from(maskWrite, (o) => (o > 0 ? 1 : 0)), a, b, plan.depth_axis, plan.display_dimensions).data; - const scaled = nearestResize(screen, rows, cols, request.image.height, request.image.width); + * when it drew none) and `overlay`, the input with the voxels the mask labels + * tinted the target colour (mask.ts stageMask: the write box's mask laid out + * as the image is and scaled to it, so it shows what accepting writes, not + * the model's own image). Every image is encoded here, so nothing the model + * sent reaches the page as it came. */ +export async function buildPreview(codec: ImageCodec, request: SegmentRequest, result: SegmentResult, overlay: RgbaImage): Promise { const [inputPng, modelPng, overlayPng] = await Promise.all([ codec.encodePng(request.image), result.model_image ? codec.encodePng(result.model_image) : Promise.resolve(null), - codec.encodePng(overlay(request.image, scaled, request.target_rgb)), + codec.encodePng(overlay), ]); return { input_png: base64(inputPng), model_png: modelPng ? base64(modelPng) : null, overlay_png: base64(overlayPng) }; } - -/** `mask` (rows × cols, rows first) at newRows × newCols by nearest - * neighbour, pixel centre to pixel centre (PIL's NEAREST). */ -export function nearestResize(mask: Uint8Array, rows: number, cols: number, newRows: number, newCols: number): Uint8Array { - if (rows === newRows && cols === newCols) return mask; - const out = new Uint8Array(newRows * newCols); - for (let r = 0; r < newRows; r++) { - const sr = Math.min(rows - 1, Math.floor(((r + 0.5) * rows) / newRows)); - for (let c = 0; c < newCols; c++) { - const sc = Math.min(cols - 1, Math.floor(((c + 0.5) * cols) / newCols)); - out[r * newCols + c] = mask[sr * cols + sc]; - } - } - return out; -} - diff --git a/web/src/backend/routes/ai-annotate.ts b/web/src/backend/routes/ai-annotate.ts index ad4cd9cd..f5eab2d5 100644 --- a/web/src/backend/routes/ai-annotate.ts +++ b/web/src/backend/routes/ai-annotate.ts @@ -580,8 +580,9 @@ export function aiAnnotateRoutes(router: Router, deps: Partial = } catch (e) { job.status = "ready"; if (e instanceof Refused || e instanceof AIAnnotateError) return answerError(ctx, e); - // Our own messages (labelsForBox, fill's ids running out), which say - // what to do: the seed route answers them the same way. + // Our own messages (a store that would not write, a box the labels do + // not fit), as the server answers its ValueErrors; fill's ids running + // out is the AIAnnotateError above, in the Python's words. ctx.log(`Could not accept AI annotation ${job.annotateId}: ${message(e)}`); return json(409, { success: false, error: message(e) }); } diff --git a/web/src/backend/routes/finetune.ts b/web/src/backend/routes/finetune.ts index 1a9b5861..3e9141da 100644 --- a/web/src/backend/routes/finetune.ts +++ b/web/src/backend/routes/finetune.ts @@ -210,7 +210,8 @@ export function finetuneRoutes(router: Router, deps: Partial = {}) }); // POST /api/finetune/create-volume {model_name}: a new volume over the - // open dataset on the model's output grid (planVolume), its zarr files in + // open dataset on the model's output grid (planVolume: the portable + // Python's plan, whose refusal is a 400), its zarr files in // the paint store, and the session's volume from now on. The answer's // URLs are the paint route's (volumeUrls); the tab then calls // add-to-viewer with neuroglancer_url, as it does on the server. @@ -227,7 +228,7 @@ export function finetuneRoutes(router: Router, deps: Partial = {}) if (!dataset) return badRequest(NO_DATASET); let volume: PaintVolume; try { - volume = d.plan(dataset, model.entry, ctx.session.state.chain, { now: d.now() }); + volume = await d.plan(ctx.infer, dataset, model.entry, ctx.session.state.chain, { now: d.now() }); } catch (e) { return badRequest(message(e)); } diff --git a/web/src/backend/routes/good-regions.ts b/web/src/backend/routes/good-regions.ts index 5bdb9c98..f92a7f38 100644 --- a/web/src/backend/routes/good-regions.ts +++ b/web/src/backend/routes/good-regions.ts @@ -11,14 +11,16 @@ // is one volume, so the file is at the volume's root in the paint store // (finetune/good-regions.ts). Without a volume a mark is refused with // needs_volume, as the server refuses one it has nowhere to save (the tab -// offers New Volume on it). +// offers New Volume on it). The region a mark makes, its size and the +// layer's boxes are the server's rules, cellmap_flow/portable/regions.py, +// called in the engine's Pyodide worker. // // The answers keep the Python routes' keys and statuses, so // static/js/dashboard/finetune/good-region-marks.js reads them unchanged. import type { AppContext, PaintVolume } from "../../contracts"; -import { defaultRegionSizeNm, isGoodRegion, loadGoodRegions, regionAt, saveGoodRegions } from "../../finetune/good-regions"; +import { loadGoodRegions, markRegion, regionBoxes, saveGoodRegions } from "../../finetune/good-regions"; import { readVolume } from "../../finetune/volume"; -import { dimensionsOf, GOOD_REGIONS_LAYER, goodRegionsLayer } from "../../viewer/layers"; +import { dimensionsOf, GOOD_REGIONS_LAYER, goodRegionsLayer, type RegionBox } from "../../viewer/layers"; import type { Router } from "../router"; import { json, objectBody } from "../router"; @@ -35,25 +37,25 @@ export function goodRegionRoutes(router: Router): void { // POST /api/finetune/good-regions/mark-view {size_nm?, label?}: a box // centred where the viewer looks (view_box_nm), one model output block by - // default, labelled good- unless a label is sent; saved, drawn, and - // answered with the region and the count. Refused as the Python refuses: - // no viewer position or a size that is not positive (400), then no - // volume to keep it with (409, needs_volume). + // default (no size, or 0), labelled good- unless a label is sent; + // saved, drawn, and answered with the region and the count. Refused as the + // Python refuses: no viewer position or a size that is not positive (400), + // then no volume to keep it with (409, needs_volume). router.post("/api/finetune/good-regions/mark-view", async (req, _params, ctx) => { const body = objectBody(req) ?? {}; const centre = ctx.viewer.position(); if (!centre) return json(400, { success: false, error: "Viewer has no position" }); const volume = await sessionVolume(ctx); - const size = sizeNm(body.size_nm, volume); - if (!size) return json(400, { success: false, error: "size_nm must be positive" }); + const regions = volume ? await loadGoodRegions(ctx.paint, volume.id) : []; + const marked = await markRegion(ctx.infer, regions, centre, body.size_nm, body.label, volume); + if ("error" in marked) return json(400, { success: false, error: marked.error }); if (!volume) return json(409, { success: false, error: NO_VOLUME, needs_volume: true }); - const regions = await loadGoodRegions(ctx.paint, volume.id); - const region = regionAt(centre, size, regions.length, typeof body.label === "string" ? body.label : undefined); + const { region, boxes } = marked; regions.push(region); await saveGoodRegions(ctx.paint, volume.id, regions); - showGoodRegions(ctx, regions); + showGoodRegions(ctx, boxes); const round = (values: number[]) => `[${values.map((v) => Math.round(v)).join(", ")}]`; - ctx.log(`Marked good region ${region.label} at ${round(centre)} nm, size ${round(size)} nm (${regions.length} total)`); + ctx.log(`Marked good region ${region.label} at ${round(centre)} nm, size ${round(region.shape_nm)} nm (${regions.length} total)`); return json(200, { success: true, region, count: regions.length, persisted: true }); }); @@ -69,7 +71,7 @@ export function goodRegionRoutes(router: Router): void { if (kept.length === regions.length) return json(404, { success: false, error: `No region ${String(id)}` }); } if (volume) await saveGoodRegions(ctx.paint, volume.id, kept); - showGoodRegions(ctx, kept); + showGoodRegions(ctx, await regionBoxes(ctx.infer, kept)); return json(200, { success: true, count: kept.length }); }); } @@ -80,22 +82,13 @@ async function sessionVolume(ctx: AppContext): Promise { return id ? readVolume(ctx.paint, id) : null; } -/** The size a mark asks for, in nm, (z, y, x): one number for every axis or - * three; the volume's output block when none is sent (good_regions.py's - * DEFAULT_REGION_SIZE_NM, 896 nm a side, without a volume); null when a - * size is not a positive number. */ -function sizeNm(value: unknown, volume: PaintVolume | null): number[] | null { - const size = value === undefined || value === null ? (volume ? defaultRegionSizeNm(volume) : [896, 896, 896]) : typeof value === "number" ? [value, value, value] : value; - if (!Array.isArray(size) || size.length !== 3 || !size.every((v) => typeof v === "number" && Number.isFinite(v) && v > 0)) return null; - return size as number[]; -} - -/** Draw `regions` as the good_regions layer (refresh_good_regions_layer): - * the viewer keeps the layer hidden if the user hid it, and shows it - * otherwise, so a mark is seen to land. Its axes are the open dataset's, - * the names the viewer's coordinate space has. */ -function showGoodRegions(ctx: AppContext, regions: readonly unknown[]): void { +/** Draw the regions' `boxes` (regionBoxes: null for an entry that is not a + * region) as the good_regions layer (refresh_good_regions_layer): the + * viewer keeps the layer hidden if the user hid it, and shows it otherwise, + * so a mark is seen to land. Its axes are the open dataset's, the names the + * viewer's coordinate space has. */ +function showGoodRegions(ctx: AppContext, boxes: readonly (RegionBox | null)[]): void { const info = ctx.infer.dataset(); const axes = info ? Object.keys(dimensionsOf(info)) : undefined; - ctx.viewer.setLayers({ [GOOD_REGIONS_LAYER]: goodRegionsLayer(regions.map((r) => (isGoodRegion(r) ? r : null)), axes) }); + ctx.viewer.setLayers({ [GOOD_REGIONS_LAYER]: goodRegionsLayer(boxes, axes) }); } diff --git a/web/src/backend/routes/rank.ts b/web/src/backend/routes/rank.ts index 770cfb49..5f3b503c 100644 --- a/web/src/backend/routes/rank.ts +++ b/web/src/backend/routes/rank.ts @@ -34,11 +34,11 @@ import { FINETUNED_REPO } from "../../contracts"; import { loadGoodRegions, saveGoodRegions } from "../../finetune/good-regions"; import { readsOneSlice } from "../../finetune/modes"; import type { PaintedChunk } from "../../finetune/types"; -import { targetTransformFor } from "../../finetune/targets"; +import { targetTransformFor } from "../../finetune/target-call"; import { CHUNK_DIR, chunkKey, createVolume, hasAffinityChannels, paintedChunkIndices, planVolume, readPaintedChunk, readVolume } from "../../finetune/volume"; import { RANK_BLOCKS, WINDOW_MARGIN } from "../../generated/cellmap-flow"; import { chooseRawLevel, geometryOf, outputShape, ratio } from "../../infer/geometry"; -import { servedIndices, servedLevel } from "../../label-it/prediction"; +import { place, type Placement, servedLevel } from "../../label-it/prediction"; import { probability } from "../../label-it/segment"; import { entryFor } from "../../models/manifest"; import { PAINT_TAB, paintLayer, paintLayerName, paintLayerUrl } from "../../viewer/layers"; @@ -162,7 +162,7 @@ export function rankRoutes(router: Router): void { } const served = ctx.infer.served().find((s) => s.entry.id === entry.id); if (!served) throw new Error(`${name} did not start`); - const to = planVolume(dataset, served.entry, ctx.session.state.chain); + const to = await planVolume(ctx.infer, dataset, served.entry, ctx.session.state.chain); await createVolume(ctx.paint, to); const carried = await carryPaint(ctx, from, to); // Good regions are boxes in nm, the same on any grid: they go with the paint. @@ -191,11 +191,12 @@ export function rankRoutes(router: Router): void { async function scorePaint(ctx: AppContext, dataset: DatasetInfo, volume: PaintVolume, indices: number[][], served: ServedModel): Promise<{ score: RankScore | null; blocks: number; read: { lo: number[]; hi: number[] }[]; outside: number }> { const entry = served.entry; const level = servedLevel(dataset, entry), block = outputShape(geometryOf(entry)); - const transform = targetTransformFor(entry), affinity = hasAffinityChannels(entry); + const transform = targetTransformFor(entry, ctx.infer), affinity = hasAffinityChannels(entry); // Each painted chunk with the model's voxel under each of its painted - // voxels (servedIndices, over the painted voxels' box, which it shrinks - // to what the level covers), and the model's blocks those lie in. - const placed: { chunk: PaintedChunk; origin: number[]; at: NonNullable> }[] = []; + // voxels (label-it's place, the Python's placement, over the painted + // voxels' box, which it shrinks to what the level covers), and the + // model's blocks those lie in. + const placed: { chunk: PaintedChunk; origin: number[]; at: Placement }[] = []; const blocks = new Map(); let outside = 0; for (const index of indices) { @@ -203,7 +204,7 @@ async function scorePaint(ctx: AppContext, dataset: DatasetInfo, volume: PaintVo const box = chunk && paintedBox(chunk); if (!chunk || !box) continue; const origin = index.map((i, a) => i * volume.chunk_size[a]); - const at = servedIndices(volume, box.lo.map((v, a) => v + origin[a]), box.hi.map((v, a) => v + origin[a]), level); + const at = await place(ctx.infer, volume, box.lo.map((v, a) => v + origin[a]), box.hi.map((v, a) => v + origin[a]), level); if (!at) { outside++; continue; } placed.push({ chunk, origin, at }); forEachPainted(chunk, origin, at, (_i, s) => { @@ -217,7 +218,7 @@ async function scorePaint(ctx: AppContext, dataset: DatasetInfo, volume: PaintVo for (const key of spread(keys, RANK_BLOCKS)) { const lo = blocks.get(key)!.map((b, a) => b * block[a]), hi = lo.map((l, a) => Math.min(l + block[a], level.shape[a])); const out = await ctx.infer.readOutput(served.view, lo, hi); - read.set(key, { lo, hi, shape: out.shape, data: logitsOf(out, served.info.output_class) }); + read.set(key, { lo, hi, shape: out.shape, data: await logitsOf(ctx, out, served.info.output_class) }); } // Each placed chunk added to the model's tallies by the Python (rank.tally, // with paint_edges for the voxels a binary model's score skips): its @@ -233,7 +234,7 @@ async function scorePaint(ctx: AppContext, dataset: DatasetInfo, volume: PaintVo const [, bz, by, bx] = r.shape, [sz, sy, sx] = s.map((v, a) => v - r.lo[a]); for (let c = 0; c < C; c++) logits[c * n + i] = r.data[((c * bz + sz) * by + sy) * bx + sx]; }); - const { target, mask } = transform(chunk); + const { target, mask } = await transform(chunk); tallies = await ctx.infer.python(`${PORTABLE}:rank_tally`, { tallies, shape: chunk.shape, channels: C, labels_dtype: chunk.labels instanceof Uint16Array ? "uint16" : "uint8", affinity }, [bytesOf(chunk.labels), bytesOf(target), bytesOf(mask), bytesOf(logits)]); @@ -245,12 +246,12 @@ async function scorePaint(ctx: AppContext, dataset: DatasetInfo, volume: PaintVo /** A model's output block as logits, whatever its activation, as the * server's ranking reads it (rank_routes._score): the output as a * probability by the served model's output class (label-it's probability, - * so a sigmoid model's 0.9 and a logit model's 2.2 are one and the same), - * clipped to [1e-6, 1 - 1e-6], then its logit. A logit model's output comes - * back as it was, within float32; a probability's is no longer read as if - * 0.1 said object. */ -function logitsOf(out: { data: Float32Array; shape: number[] }, outputClass: string | null | undefined): Float32Array { - const p = probability(out, outputClass, null).data; + * the Python's, so a sigmoid model's 0.9 and a logit model's 2.2 are one + * and the same), clipped to [1e-6, 1 - 1e-6], then its logit. A logit + * model's output comes back as it was, within float32; a probability's is + * no longer read as if 0.1 said object. `out` is handed to the worker. */ +async function logitsOf(ctx: AppContext, out: { data: Float32Array; shape: number[] }, outputClass: string | null | undefined): Promise { + const p = (await probability(ctx.infer, out, outputClass)).data; for (let i = 0; i < p.length; i++) { const q = Math.min(Math.max(p[i], 1e-6), 1 - 1e-6); p[i] = Math.log(q / (1 - q)); @@ -283,9 +284,9 @@ async function rawWindow(ctx: AppContext, dataset: DatasetInfo, entry: ModelEntr /** `visit(i, served)` for each painted voxel of `chunk` the model's level * covers: `i` its index in the chunk, `served` the model's voxel under it, - * from `at` (servedIndices over a box of the volume, `origin` the chunk's - * first voxel). */ -function forEachPainted(chunk: PaintedChunk, origin: number[], at: NonNullable>, visit: (i: number, served: number[]) => void): void { + * from `at` (place over a box of the volume, `origin` the chunk's first + * voxel). */ +function forEachPainted(chunk: PaintedChunk, origin: number[], at: Placement, visit: (i: number, served: number[]) => void): void { const [, h, w] = chunk.shape; for (let z = at.lo[0]; z < at.hi[0]; z++) for (let y = at.lo[1]; y < at.hi[1]; y++) for (let x = at.lo[2]; x < at.hi[2]; x++) { const i = ((z - origin[0]) * h + y - origin[1]) * w + x - origin[2]; diff --git a/web/src/backend/routes/view-labels.ts b/web/src/backend/routes/view-labels.ts index 64a351b3..ef1e0a94 100644 --- a/web/src/backend/routes/view-labels.ts +++ b/web/src/backend/routes/view-labels.ts @@ -6,10 +6,15 @@ // gives them. // // - POST .../seed: the chosen model's prediction there, segmented into -// objects (label-it/segment.ts: threshold and connected components, or -// the mutex watershed for an affinity model), each written with an id of -// its own (2 and up), background 1. The user then cleans it up with the -// brush, which is far quicker than painting a whole object from nothing. +// objects (threshold and connected components, or the mutex watershed +// for an affinity model), each written with an id of its own (2 and up), +// background 1. The user then cleans it up with the brush, which is far +// quicker than painting a whole object from nothing. The seed's maths is +// the Python the server runs (cellmap_flow/portable/label_it.py, called +// through label_it_calls.py in the engine's Pyodide worker): the +// request's options, the channels it reads, where the prediction lies on +// the volume's grid, the probability and the components; only the mutex +// watershed itself runs here (label-it/segment.ts says why). // - POST .../background: every unannotated voxel 1, for a region the model // fills with false positives. // - POST .../split: the box's foreground relabelled by connected component, @@ -22,8 +27,13 @@ // // Seed and All Background fill only unannotated voxels (a voxel the user // painted is a decision, a seed is a guess); Split writes only foreground -// voxels whose id changes. Every write goes into the paint store -// (ai-annotate/fill.ts) and pushes an undo entry onto the volume's stack. +// voxels whose id changes. Those rules, the voxels a box covers, the ids a +// seed or a split gives and what an undo puts back are the Python the +// server's routes run too (cellmap_flow/portable/fill.py, called through +// fill_calls.py in the engine's Pyodide worker), handed the box as the paint +// store holds it; the box is read and the answer written back here +// (ai-annotate/fill.ts readBox and writeBox), and every write pushes an undo +// entry onto the volume's stack, as the AI annotation's Accept does. // The stacks are this module's, as the server's _undo_stack is // view_labels.py's, and the AI annotation's Accept pushes onto the same one // (ai_annotate.py imports it; createRouter hands it to routes/ai-annotate.ts), @@ -35,30 +45,33 @@ // - The prediction is the page's own (Inference.readOutput: the model // stage's output, the chain's normalizers and none of its postprocessors, // as the server reads its model server's array with no postprocessing), -// placed on the volume's grid by label-it/prediction.ts. The "running -// servers" are the served models, finetuned ones among them, and the -// default is the volume model's latest finetune served, named -// `-ft` here (models/finetuned.ts) where the server's are -// `_finetuned...`, else the model itself, else the only one served. +// read by label-it/prediction.ts. The "running servers" are the served +// models, finetuned ones among them, and the default is the volume +// model's latest finetune served, named `-ft` here +// (models/finetuned.ts) where the server's are `_finetuned...`, +// else the model itself, else the only one served. // - The methods: components for every model, the mutex watershed for an -// affinity model; label-it/segment.ts says why distance_watershed and -// instances are not offered. +// affinity model (methodsFor says why distance_watershed and instances +// are not offered). An affinity model's offsets are the ones its channel +// names give, by the server's rule for a model whose script says none +// (label_it.affinity_offsets). // - No MinIO: nothing to check is serving, nothing to pull to disk after. // - A seed reads the dataset the engine has open, so a volume painted on -// another dataset is refused, as an AI annotation run is. +// another dataset is refused, as an AI annotation run is; and the Python +// runs in that dataset's engine, so with no dataset open every tool here +// is refused before anything else (409), Undo before it takes the entry. // // The refusals keep the Python's order, statuses and words (where a word // was the server's own, "server", the browser's), and the answers its keys: // the page keys on `needs_volume` and `needs_confirmation`. -import { boxVoxels, paintBox, restoreBox, rewriteForeground } from "../../ai-annotate/fill"; -import { relabelObjects, seedLabels } from "../../ai-annotate/labels"; -import { AIAnnotateError, type Labels, type UndoEntry } from "../../ai-annotate/types"; -import type { AppContext, PaintVolume, ServedModel } from "../../contracts"; -import { readVolume } from "../../finetune/volume"; +import { pushUndo, readBox, writeBox } from "../../ai-annotate/fill"; +import type { Labels, UndoEntry } from "../../ai-annotate/types"; +import type { AppContext, ModelEntry, PaintVolume, ServedModel } from "../../contracts"; +import { hasAffinityChannels, readVolume } from "../../finetune/volume"; import { PYTHON } from "../../generated/python-data"; -import { asConnectivity, asBoolLike, asInt } from "../../infer/label"; -import { readPrediction, servedLevel } from "../../label-it/prediction"; -import { methodsFor, outputTypeOf, SEED_METHODS, type SeedMethod, seedPlan, segmentPrediction } from "../../label-it/segment"; +import { asConnectivity, asBoolLike } from "../../infer/label"; +import { type Placement, placementArgs, readRegion, servedLevel } from "../../label-it/prediction"; +import { type Block, bufferOf, LABEL_IT, mutexObjects } from "../../label-it/segment"; import { paintLayerName } from "../../viewer/layers"; import type { Router } from "../router"; import { json, message, objectBody } from "../router"; @@ -106,22 +119,32 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { return id ? readVolume(ctx.paint, id) : null; } - /** The session's volume and the box's voxels in it (_target_box). */ + /** The session's volume and the box's voxels in it (_target_box): the + * voxels whose centres lie in the box on screen, by the Python's + * fill.box_voxels. */ async function targetBox(ctx: AppContext, body: Record): Promise<{ volume: PaintVolume; lo: number[]; hi: number[] }> { const volume = await sessionVolume(ctx); if (!volume) throw new Refused("No annotation volume to label. Create or resume one first.", 409, { needs_volume: true }); let sizeNm = body.size_nm; if (body.box_voxels !== undefined && body.box_voxels !== null) sizeNm = boxNm(body.box_voxels, volume); const { centre, size } = viewBoxNm(ctx, sizeNm, volume); - const box = boxVoxels(volume, centre.map((c, a) => c - size[a] / 2), size, volume.dataset_shape_voxels); + const box = await ctx.infer.python<{ lo: number[]; hi: number[] } | null>(`${FILL}:box`, { + volume: { output_voxel_size: volume.output_voxel_size, dataset_offset_nm: volume.dataset_offset_nm ?? null }, + offset_nm: centre.map((c, a) => c - size[a] / 2), + shape_nm: size, + volume_shape: volume.dataset_shape_voxels, + }); if (!box) throw new Refused("The view is outside the annotation volume.", 400); return { volume, lo: box.lo, hi: box.hi }; } - /** Fill the box's unannotated voxels with `labelsFor`'s labels (_fill): the answer's counts. */ - async function fill(ctx: AppContext, volume: PaintVolume, lo: number[], hi: number[], labelsFor: (existing: Labels) => Labels) { + /** Fill the box's unannotated voxels by fill_calls.py's `call` (seed or + * background, with `args` and `arrays` beside the box; _fill): the + * answer's counts. */ + async function fill(ctx: AppContext, volume: PaintVolume, lo: number[], hi: number[], call: string, args: Record = {}, arrays: ArrayBuffer[] = []) { const stack = undoStack(volume.id); - const counts = await paintBox(ctx.paint, volume, lo, hi, labelsFor, { overwrite: false, undo: stack }); + const { value: counts, before, after } = await onBox(ctx, volume, lo, hi, call, args, arrays); + if (after) pushUndo(stack, { lo, hi, before, after }); const wrote = counts.n_foreground + counts.n_background > 0; // The viewer shows the chunks it already read until the paint layer is read again. const layerRefreshed = wrote ? ctx.viewer.refreshLayer(paintLayerName(volume.id)) : false; @@ -141,8 +164,6 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { /** A refusal's answer, or the 500 of anything else (as the Python's catch-all). */ function answerError(ctx: AppContext, e: unknown, what: string) { if (e instanceof Refused) return json(e.status, { success: false, error: e.message, ...e.extra }); - // fill's ids running out (freshIds): a ValueError there, 400. - if (e instanceof AIAnnotateError) return json(400, { success: false, error: e.userMessage }); ctx.log(`Error ${what}: ${message(e)}`); return json(500, { success: false, error: message(e) }); } @@ -158,7 +179,7 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { const volumeId = ctx.session.state.paintVolume; const volume = await sessionVolume(ctx); const sources = ctx.infer.served(); - const methods: Record = {}; + const methods: Record = {}; for (const s of sources) methods[s.entry.name] = methodsFor(outputTypeOf(s.entry)); return json(200, { success: true, @@ -182,20 +203,13 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { router.post(`${BASE}/seed`, async (req, _params, ctx) => { const body = objectBody(req) ?? {}; try { - const threshold = body.threshold; - if (threshold !== undefined && threshold !== null) { - const t = pyFloat(threshold); - if (!(t >= 0 && t <= 1)) throw new Refused(`threshold must be between 0 and 1, got ${pyStr(threshold)}`, 400); - } - const minSize = valueOr400(() => asInt(body.min_size as never, 0, "min_size")); - if (minSize < 0) throw new Refused(`min_size cannot be negative, got ${minSize}`, 400); - const method = body.method || "components"; - if (typeof method !== "string" || !(SEED_METHODS as readonly string[]).includes(method)) { - throw new Refused(`method must be one of ${SEED_METHODS.join(", ")}, got ${typeof method === "string" ? `'${method}'` : pyStr(method)}`, 400); - } - const connectivity = valueOr400(() => asConnectivity((body.connectivity ?? 1) as never, 3)); - const perSlice = asBoolLike(body.per_slice as never); - const selectChannel = body.select_channel === undefined || body.select_channel === null ? null : valueOr400(() => asInt(body.select_channel as never, 0, "select_channel")); + // The Python runs in the open dataset's engine, and a seed reads that + // dataset: without one there is nothing to seed from. + const dataset = requireDataset(ctx); + // The request's options as the server reads them, its defaults and its + // refusals (label_it.seed_options), before anything else, as there. + const { options } = refusedOr(await ctx.infer.python<{ options: SeedOptions } | Refusal>(`${LABEL_IT}:seed_options`, body)); + const method = options.method; const { volume, lo, hi } = await targetBox(ctx, body); const confirmation = confirmationNeeded(body, lo, hi); @@ -206,40 +220,39 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { if (!served) throw new Refused(`${base} is not served. Serve it from the Models tab, or pick a model.`); // The prediction is the open dataset's: a volume painted on another // one would get it at the wrong place. - const dataset = ctx.infer.dataset(); - if (!dataset) throw new Refused("No dataset is open. Open the one the annotation volume was painted on."); if (dataset.url !== volume.dataset_path) { throw new Refused(`The annotation volume was painted on ${volume.dataset_path}, but the viewer shows ${dataset.url}: open that dataset again, or make a new volume on this one.`); } const name = served.entry.name; - const plan = valueOr400(() => seedPlan(served.entry, selectChannel)); - const offered = methodsFor(plan.outputType); - if (!(offered as string[]).includes(method)) throw new Refused(`${name} cannot seed by ${method}; it offers ${offered.join(", ")}`, 400); + // What the seed reads of the model (label_it.seed_channels, the + // offsets by its channel names) and where the box lies on its level + // (label_it.placement), by the Python. + const plan = refusedOr(await ctx.infer.python(`${LABEL_IT}:seed_plan`, { + channel_names: served.entry.manifest.channels, + select_channel: body.select_channel ?? null, + ...placementArgs(volume, lo, hi, servedLevel(dataset, served.entry)), + })); + const outputType = outputTypeOf(served.entry); + const offered = methodsFor(outputType); + if (!offered.includes(method)) throw new Refused(`${name} cannot seed by ${method}; it offers ${offered.join(", ")}`, 400); + const placement = plan.placement; + if (!placement) throw new Refused("The view is outside what the model predicts.", 400); - let read: Awaited>; + let region: Block; try { - read = await readPrediction(ctx.infer, served.view, servedLevel(dataset, served.entry), volume, lo, hi); + region = await readRegion(ctx.infer, served.view, placement); } catch (e) { ctx.log(`Could not read the prediction to seed from: ${message(e)}`); throw new Refused(`Could not read the model's prediction: ${message(e)}`, 502); } - if (!read) throw new Refused("The view is outside what the model predicts.", 400); - const objects = valueOr400(() => - segmentPrediction(method, read!.prediction, served.info.output_class, plan.channels, plan.offsets, { - threshold: threshold === undefined || threshold === null ? 0.5 : pyFloat(threshold), - minSize, - connectivity, - perSlice, - }), - ); + const objects = await seedObjects(ctx, options, region, placement, plan, served.info.output_class); // An instance target needs ids apart from its neighbours'; a selected - // channel trains as a binary target. Ids counting up need room: an - // instance target on a uint16 volume gets them, a uint8 volume reuses - // free ids. - const instanceTarget = plan.outputType === "affinities" && selectChannel === null; - const labelsFor = (existing: Labels) => seedLabels(objects, existing, instanceTarget && existing instanceof Uint16Array); - const answer = await fill(ctx, volume, read.lo, read.hi, labelsFor); - return json(200, { ...answer, model: name, threshold: threshold ?? null, method }); + // channel trains as a binary target. Ids counting up need room: the + // Python gives them to an instance target on a uint16 volume, and a + // uint8 volume reuses free ids (fill.counts_up). + const instanceTarget = outputType === "affinities" && (body.select_channel === undefined || body.select_channel === null); + const answer = await fill(ctx, volume, placement.lo, placement.hi, "seed", { instance_target: instanceTarget }, [bytesOf(objects)]); + return json(200, { ...answer, model: name, threshold: body.threshold ?? null, method }); } catch (e) { return answerError(ctx, e, "seeding the view from the prediction"); } @@ -251,17 +264,18 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { router.post(`${BASE}/background`, async (req, _params, ctx) => { const body = objectBody(req) ?? {}; try { + requireDataset(ctx); const { volume, lo, hi } = await targetBox(ctx, body); const confirmation = confirmationNeeded(body, lo, hi); if (confirmation) return confirmation; - return json(200, await fill(ctx, volume, lo, hi, (existing) => (existing instanceof Uint16Array ? new Uint16Array(existing.length) : new Uint8Array(existing.length)).fill(1))); + return json(200, await fill(ctx, volume, lo, hi, "background")); } catch (e) { return answerError(ctx, e, "labelling the view background"); } }); // POST .../split: relabel the box on screen's foreground by connected - // component (labels.ts relabelObjects). For a merge the model made: paint + // component (fill.relabel_objects). For a merge the model made: paint // a background wall through the object, in every slice it spans, and // click; the two sides get ids of their own. A stroke joining two objects // merges them. JSON body, all optional: size_nm or box_voxels, and @@ -271,6 +285,7 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { router.post(`${BASE}/split`, async (req, _params, ctx) => { const body = objectBody(req) ?? {}; try { + requireDataset(ctx); const connectivity = valueOr400(() => asConnectivity((body.connectivity ?? 1) as never, 3)); const perSlice = asBoolLike(body.per_slice as never); const { volume, lo, hi } = await targetBox(ctx, body); @@ -278,7 +293,9 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { if (confirmation) return confirmation; const shape = hi.map((h, a) => h - lo[a]); const stack = undoStack(volume.id); - const { changed, counts } = await rewriteForeground(ctx.paint, volume, lo, hi, (existing) => relabelObjects(existing, shape, { connectivity, perSlice }), { undo: stack }); + const { value, before, after } = await onBox(ctx, volume, lo, hi, "split", { connectivity, per_slice: perSlice }); + if (after) pushUndo(stack, { lo, hi, before, after }); + const { changed, ...counts } = value; const layerRefreshed = changed ? ctx.viewer.refreshLayer(paintLayerName(volume.id)) : false; ctx.log(`Relabelled box ${lo.join(",")}..${hi.join(",")} of ${volume.id}: ${counts.objects} objects, ${counts.split} split off, ${counts.merged} merged, ${changed} voxels changed`); return json(200, { @@ -302,12 +319,15 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { // stroke painted there since is kept. 409 when there is nothing to undo. router.post(`${BASE}/undo`, async (_req, _params, ctx) => { try { + // Before the entry is taken: an undo the Python cannot run keeps it. + requireDataset(ctx); const volume = await sessionVolume(ctx); if (!volume) throw new Refused("No annotation volume to undo in."); const stack = undoStack(volume.id); const entry = stack.pop(); if (!entry) throw new Refused("Nothing to undo."); - const restored = await restoreBox(ctx.paint, volume, entry); + const { value } = await onBox<{ restored: number }>(ctx, volume, entry.lo, entry.hi, "undo", {}, [bytesOf(entry.before), bytesOf(entry.after)]); + const restored = value.restored; const layerRefreshed = restored ? ctx.viewer.refreshLayer(paintLayerName(volume.id)) : false; ctx.log(`Undid the label action on box ${entry.lo.join(",")}..${entry.hi.join(",")} of ${volume.id}: ${restored} voxels restored`); return json(200, { success: true, restored, reload_viewer: restored > 0, layer_refreshed: layerRefreshed, can_undo: stack.length > 0 }); @@ -319,6 +339,153 @@ export function viewLabelsRoutes(router: Router): ViewLabelsState { return state; } +// --------------------------------------------------------------------------- +// The box's labels, by the Python +// --------------------------------------------------------------------------- + +/** What the box tools call: cellmap_flow/portable/fill_calls.py, over fill.py. */ +const FILL = "cellmap_flow.portable.fill_calls"; + +const NO_DATASET = "No dataset is open. Open the one the annotation volume was painted on."; + +/** The open dataset, in whose engine the Python runs (Inference.python): + * without one no tool here can run, refused as a seed is refused (409). */ +function requireDataset(ctx: AppContext) { + const dataset = ctx.infer.dataset(); + if (!dataset) throw new Refused(NO_DATASET); + return dataset; +} + +/** What a fill wrote (fill.paint's counts, as fill_calls.seed and + * background answer them): the voxels written with foreground (2 and up) + * and with background, and those of them that held a label before (none, + * as these fill only unannotated voxels). The routes answer the first two + * as filled_foreground and filled_background. */ +interface PaintCounts { + n_foreground: number; + n_background: number; + n_overwritten: number; +} + +/** What a split did (fill_calls.split): the components found, those given a + * fresh id (`split`), those that held more than one id (`merged`), and the + * voxels whose label changed. */ +interface SplitCounts { + objects: number; + split: number; + merged: number; + changed: number; +} + +/** A typed array's own bytes, copied: a call's arrays are handed to the + * worker, and the box read here stays the undo entry's. */ +function bytesOf(a: ArrayBufferView): ArrayBuffer { + return a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength) as ArrayBuffer; +} + +/** Run fill_calls.py's `call` on the box [lo, hi) of `volume` as the paint + * store holds it now (readBox; sent first, `arrays` after it), and write + * the box it answers back into the store (writeBox), when it answers one: + * it answers none when nothing changes, and nothing is written then. + * Returns the call's value, the box as it was (`before`) and, when written, + * as it is now (`after`, else null), for the caller's undo entry. A refusal + * (new ids that do not fit in the volume's dtype) is thrown as the 400 the + * server answers. */ +async function onBox( + ctx: AppContext, + volume: PaintVolume, + lo: number[], + hi: number[], + call: string, + args: Record, + arrays: ArrayBuffer[] = [], +): Promise<{ value: V; before: Labels; after: Labels | null }> { + const before = await readBox(ctx.paint, volume, lo, hi); + const answer = await ctx.infer.python<{ value: V; arrays: ArrayBuffer[] } | Refusal>( + `${FILL}:${call}`, + { ...args, shape: hi.map((h, a) => h - lo[a]), dtype: volume.dtype }, + [bytesOf(before), ...arrays], + ); + const { value, arrays: written } = refusedOr(answer); + if (!written.length) return { value, before, after: null }; + const after = volume.dtype === "uint16" ? new Uint16Array(written[0]) : new Uint8Array(written[0]); + await writeBox(ctx.paint, volume, lo, hi, after); + return { value, before, after }; +} + +// --------------------------------------------------------------------------- +// The seed +// --------------------------------------------------------------------------- + +/** A refusal the Python answers in place of its value: the message the + * server answers 400 with (a ValueError there). */ +interface Refusal { + error: string; +} + +/** `answer`, or its refusal thrown as the 400 the server answers. */ +function refusedOr(answer: T | Refusal): T { + if ("error" in answer) throw new Refused(answer.error, 400); + return answer; +} + +/** A seed's options, as label_it.seed_options reads them from the body. */ +interface SeedOptions { + method: string; + threshold: number; + min_size: number; + connectivity: number; + per_slice: boolean; +} + +/** What a seed reads (label_it_calls.seed_plan): `channels` [start, stop) + * (null: every one), an affinity model's `offsets`, and where the box lies + * on the model's level (null: it does not). */ +interface SeedPlan { + channels: [number, number] | null; + offsets: number[][] | null; + placement: Placement | null; +} + +/** What a model is trained on, as the browser tells it (the server's + * autodetect_output_type, by its channel-name test): "affinities" for a + * model with a channel named `*_aff*`, else "binary". */ +type OutputType = "affinities" | "binary"; + +function outputTypeOf(entry: ModelEntry): OutputType { + return hasAffinityChannels(entry) ? "affinities" : "binary"; +} + +/** The seed methods that fit a model's output, the best fit first + * (view_labels.methods_for): the mutex watershed for affinities, then + * components, which fits every output and so is always offered, last. + * + * The server's other two fit outputs no browser model has: + * "distance_watershed" (a distance map's watershed from its h-maxima) and + * "instances" (a model that serves integer ids, Cellpose's masks). The + * browser's models are those of Hugging Face's cellmap/onnx-web-models, + * affinity models (channels mito_aff_z/y/x) and binary ones (nuc, er), all + * raw logits, so neither is ever offered, and a request for one is refused + * as the server refuses a method the model does not offer (the page lists + * them greyed out, saying what they need). */ +function methodsFor(outputType: OutputType): string[] { + return outputType === "affinities" ? ["mutex_watershed", "components"] : ["components"]; +} + +/** The objects the seed's method makes of `region` (the served box + * `placement` reads), 0 background and 1..n, over the placement's box, z, + * y, x: for components, the Python's (label_it_calls.seed); for the mutex + * watershed, its input from the Python and the watershed here + * (label-it/segment.ts mutexObjects). `region` is handed to the worker. */ +async function seedObjects(ctx: AppContext, options: SeedOptions, region: Block, placement: Placement, plan: SeedPlan, outputClass: string | null | undefined): Promise { + const args = { ...options, output_class: outputClass ?? null, channels: plan.channels, offsets: plan.offsets, indices: placement.indices, shape: region.shape }; + const out = refusedOr(await ctx.infer.python<{ value: { shape?: number[] }; arrays: ArrayBuffer[] } | Refusal>(`${LABEL_IT}:seed`, args, [bufferOf(region.data)])); + if (options.method === "mutex_watershed") { + return mutexObjects({ data: new Float32Array(out.arrays[0]), shape: out.value.shape! }, plan.offsets!, options.threshold, options.min_size); + } + return new Uint32Array(out.arrays[0]); +} + // --------------------------------------------------------------------------- // The box and the model // --------------------------------------------------------------------------- @@ -402,7 +569,7 @@ function predictionSource(sources: ServedModel[], base: string, chosen: string | // --------------------------------------------------------------------------- /** `fn`'s value, its error a 400 with the error's message: the Python's - * ValueError from parsing a field or planning the seed. */ + * ValueError from parsing a field. */ function valueOr400(fn: () => T): T { try { return fn(); @@ -412,16 +579,6 @@ function valueOr400(fn: () => T): T { } } -/** Python's float() of a body value: a number as it is, a bool as 0 or 1, a - * string when it is a float literal (whitespace around it allowed), and NaN - * for anything float() refuses, which the caller refuses in turn. */ -function pyFloat(value: unknown): number { - if (typeof value === "number") return value; - if (typeof value === "boolean") return value ? 1 : 0; - if (typeof value === "string" && /^\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?\s*$/.test(value)) return Number(value); - return NaN; -} - /** A value as Python's str() shows it in a refusal: a list as `[1, 2]`, None for null. */ function pyStr(value: unknown): string { if (value === null || value === undefined) return "None"; diff --git a/web/src/finetune/full/run-full.ts b/web/src/finetune/full/run-full.ts index 71264bfa..2b67c84e 100644 --- a/web/src/finetune/full/run-full.ts +++ b/web/src/finetune/full/run-full.ts @@ -51,7 +51,7 @@ import { AugmentTally, augmentIntensity, drawAugmentation, intensityRange, trans import { entryFor } from "../../models/manifest"; import { readsOneSlice, refuseMode, sourceOf } from "../modes"; import { readGraph, readModel } from "../onnx-proto"; -import { targetTransformFor } from "../targets"; +import { targetTransformFor } from "../target-call"; import type { Finetuned, FinetuneOptions, FinetuneRun, RunFinetune } from "../types"; import { buildNet, type InitializerOffsets, readOnnxWeights, readTorchParams, toOnnxWeights, writeOnnxWeights } from "./cellmap-tfjs-train"; import { type FetchFullFiles, fetchFullFiles, type TorchParams } from "./files"; @@ -253,13 +253,13 @@ export function createRunFull(deps: FullDeps = { loadTf, fetchFiles: fetchFullFi ? `Augmentation on: each step's tiles get a brightness ×0.8 to ×1.2 and noise of 1% of ${range ? `the raw's range (${range[0]} to ${range[1]})` : "each tile's largest value"} on the raw before it is normalized, then random flips and quarter turns of the raw and the paint alike, with the targets${lambda > 0 ? " and the starting network's logits" : ""} made after` : "Augmentation off"); - const transform = deps.targetTransformFor(entry); + const transform = deps.targetTransformFor(entry, run.infer); const samples: TileSample[] = []; let distilled = 0; for (const tile of tiles) { if (report.cancelled()) throw new Error("cancelled before training"); const labels = tileLabels(tile, output, chunks, volume); - const t = tileTargets(labels, output, transform); + const t = await tileTargets(labels, output, transform); if (!t.masked) continue; // paint the loss cannot count here (an affinity needs a painted pair) const { raw, inside } = await tileRaw(tile, side, output, run.infer, dataset, geo.rawLevel); samples.push(options.augment @@ -302,7 +302,7 @@ export function createRunFull(deps: FullDeps = { loadTf, fetchFiles: fetchFullFi const rows = Math.min(batch, samples.length - from); let masked = 0; for (let r = 0; r < rows; r++) { - const s = augmented(samples[order[from + r]]); + const s = await augmented(samples[order[from + r]]); x.set(s.input, r * s.input.length); t.set(s.target, r * s.target.length); m.set(s.mask, r * s.mask.length); @@ -365,12 +365,12 @@ export function createRunFull(deps: FullDeps = { loadTf, fetchFiles: fetchFullFi /** `s` as this step trains on it: as read, or with augmentation a new * draw of its raw and paint, normalized, turned, and its targets made * from the turned paint. */ - function augmented(s: TileSample): Pick { + async function augmented(s: TileSample): Promise> { if (!s.raw || !s.inside || !s.labels) return s; const draw = drawAugmentation(augmentRandom, turnable(s.tile.depth)); tally.add(draw); const input = transformBlock(normalizeTile(augmentIntensity(s.raw, s.inside, draw.scale, range, augmentRandom), s.inside, norm, [1, side, side]), [1, side, side], draw); - const t = tileTargets(transformBlock(s.labels, [1, output, output], draw), output, transform); + const t = await tileTargets(transformBlock(s.labels, [1, output, output], draw), output, transform); return { input, target: t.target, mask: t.mask, masked: t.masked }; } }; @@ -438,14 +438,14 @@ async function trainBlocks(run: FinetuneRun, deps: FullDeps): Promise ? `Augmentation on: each step's block gets a brightness ×0.8 to ×1.2 and noise of 1% of ${range ? `the raw's range (${range[0]} to ${range[1]})` : "each block's largest value"} on the raw before it is normalized, then random flips along z, y and x${turnable ? " and quarter turns in y–x" : ""} of the raw and the paint alike, with the targets${lambda > 0 ? " and the starting network's logits" : ""} made after` : "Augmentation off"); - const transform = deps.targetTransformFor(entry); + const transform = deps.targetTransformFor(entry, run.infer); const cube = [output, output, output] as const; const samples: BlockSample[] = []; let distilled = 0; for (const block of blocks) { if (report.cancelled()) throw new Error("cancelled before training"); const labels = blockLabels(block, output, chunks, volume); - const t = blockTargets(labels, cube, transform); + const t = await blockTargets(labels, cube, transform); if (!t.masked) continue; // paint the loss cannot count here (an affinity needs a painted pair) const { raw, inside } = await blockRaw(block, side, output, run.infer, dataset, geo.rawLevel); samples.push(options.augment @@ -480,7 +480,7 @@ async function trainBlocks(run: FinetuneRun, deps: FullDeps): Promise shuffle(order, random); let sum = 0, sumSupervised = 0, sumDistillation = 0, weight = 0; for (const i of order) { - const s = augmented(samples[i]); + const s = await augmented(samples[i]); const xs = tf.tensor5d(s.input, [1, side, side, side, 1]); const ts = tf.tensor5d(Float32Array.from(s.target), [1, output, output, output, channels]); const ms = tf.tensor5d(Float32Array.from(s.mask), [1, output, output, output, channels]); @@ -536,12 +536,12 @@ async function trainBlocks(run: FinetuneRun, deps: FullDeps): Promise /** `s` as this step trains on it: as read, or with augmentation a new * draw of its raw and paint, normalized, flipped and turned, and its * targets made from the turned paint. */ - function augmented(s: BlockSample): Pick { + async function augmented(s: BlockSample): Promise> { if (!s.raw || !s.inside || !s.labels) return s; const draw = drawAugmentation(augmentRandom, turnable); tally.add(draw); const input = transformBlock(normalizeTile(augmentIntensity(s.raw, s.inside, draw.scale, range, augmentRandom), s.inside, norm, [side, side, side]), [side, side, side], draw); - const t = blockTargets(transformBlock(s.labels, cube, draw), cube, transform); + const t = await blockTargets(transformBlock(s.labels, cube, draw), cube, transform); return { input, target: t.target, mask: t.mask, masked: t.masked }; } } diff --git a/web/src/finetune/full/tiles.ts b/web/src/finetune/full/tiles.ts index b1282061..243a7671 100644 --- a/web/src/finetune/full/tiles.ts +++ b/web/src/finetune/full/tiles.ts @@ -346,16 +346,16 @@ export async function tileInput(tile: Tile, side: number, output: number, infer: } /** A tile's targets from its labels, through the model's transform - * (targets.ts) on the tile as a one-slice chunk, channels last. */ -export function tileTargets(labels: Uint8Array | Uint16Array, output: number, transform: TargetTransform): { target: Uint8Array; mask: Uint8Array; channels: number; masked: number } { + * (target-call.ts) on the tile as a one-slice chunk, channels last. */ +export function tileTargets(labels: Uint8Array | Uint16Array, output: number, transform: TargetTransform): Promise<{ target: Uint8Array; mask: Uint8Array; channels: number; masked: number }> { return blockTargets(labels, [1, output, output], transform); } /** The targets of labels of `shape` (z, y, x; a tile's is one slice), through - * the model's transform (targets.ts) on them as a chunk, channels last: a + * the model's transform (target-call.ts) on them as a chunk, channels last: a * cube's affinities pair z neighbours too. */ -export function blockTargets(labels: Uint8Array | Uint16Array, shape: readonly [number, number, number], transform: TargetTransform): { target: Uint8Array; mask: Uint8Array; channels: number; masked: number } { - const t = transform({ index: [0, 0, 0], shape: [...shape], labels }); +export async function blockTargets(labels: Uint8Array | Uint16Array, shape: readonly [number, number, number], transform: TargetTransform): Promise<{ target: Uint8Array; mask: Uint8Array; channels: number; masked: number }> { + const t = await transform({ index: [0, 0, 0], shape: [...shape], labels }); const n = shape[0] * shape[1] * shape[2], C = t.channels; const target = new Uint8Array(n * C), mask = new Uint8Array(n * C); let masked = 0; diff --git a/web/src/finetune/good-regions.ts b/web/src/finetune/good-regions.ts index c0105154..33573413 100644 --- a/web/src/finetune/good-regions.ts +++ b/web/src/finetune/good-regions.ts @@ -1,9 +1,13 @@ // Good regions: boxes the user marked (Mark as Good) where the model already -// gets it right, the counterweight to the paint. The port of the server's -// dashboard/routes/finetune/good_regions.py (the region a mark makes), -// finetune/session/manifest.py (good_regions.json, read when a run starts) -// and finetune/data/sampler.py with dataset.py (where a region anchors the -// model) for the browser. +// gets it right, the counterweight to the paint. Their rules are the +// server's, cellmap_flow/portable/regions.py, run in the engine's Pyodide +// worker (volume_calls.py): the region a mark makes and its size +// (good_regions.py's mark-view), which stored entries are regions (what the +// layer draws and the trainer places), and where a region anchors the model +// on the volume's grid (the server trainer's rehearsal patch, +// finetune/data/sampler.py with PatchReader.snap). This file keeps the +// regions' file and turns the anchors into the browser trainer's per-chunk +// masks (anchorChunks), which only its trainer has. // // Where they are kept. The server keeps a session's regions in // good_regions.json beside its corrections directory; a browser session is @@ -15,31 +19,23 @@ // volume's regions read as the server's do. // // What a region holds. The server draws a rehearsal patch for a region: -// one model output block centred exactly on the region's centre, snapped to -// whole voxels (PatchReader.snap), and holds its unpainted voxels to the -// teacher (the anchor mask, `annotation == 0`, so a scribble inside a good -// region wins: the region stays correctable). anchorBoxes makes the same -// box on the volume's grid, whose blocks are the model's output blocks too, -// so a region marked at the default size, one output block, anchors the -// block it was marked over and no more. A region whose centre is outside -// the volume is skipped, as the server skips one (marked on another volume, -// it would anchor the model to padding). -import { volumeCornerNm } from "../ai-annotate/fill"; -import type { PaintStore, PaintVolume } from "../contracts"; +// one model output block centred on the region's centre, snapped to whole +// voxels, and holds its unpainted voxels to the teacher (the anchor mask, +// `annotation == 0`, so a scribble inside a good region wins: the region +// stays correctable). anchorBoxes makes the same box on the volume's grid, +// whose blocks are the model's output blocks too, so a region marked at the +// default size, one output block, anchors the block it was marked over and +// no more. A region whose centre is outside the volume is skipped, as the +// server skips one (marked on another volume, it would anchor the model to +// padding). +import type { Inference, PaintStore, PaintVolume } from "../contracts"; +import type { RegionBox } from "../viewer/layers"; import type { GoodRegion } from "./types"; +import { VOLUME_CALLS } from "./volume"; /** The regions' file, at the volume's root (manifest.GOOD_REGIONS_FILENAME). */ export const GOOD_REGIONS_FILE = "good_regions.json"; -/** Whether `value` is a region the trainer can place: an id, a label, and a - * box of three finite numbers each, its size positive. */ -export function isGoodRegion(value: unknown): value is GoodRegion { - if (!value || typeof value !== "object") return false; - const r = value as Record; - const triple = (v: unknown, positive: boolean) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number" && Number.isFinite(n) && (!positive || n > 0)); - return typeof r.id === "string" && typeof r.label === "string" && triple(r.offset_nm, false) && triple(r.shape_nm, true); -} - /** The regions of volume `id`, as stored ([] when there are none, or the * file is not a list: manifest.load_good_regions's rule). An entry that is * not a region is kept as it is, so that a list rewritten after a mark or a @@ -60,78 +56,69 @@ export async function saveGoodRegions(store: PaintStore, id: string, regions: un await store.put(id, GOOD_REGIONS_FILE, new TextEncoder().encode(JSON.stringify(regions, null, 2))); } -/** A new region's id: eight hex digits, as the server's uuid4().hex[:8]. */ -export function newRegionId(): string { - const bytes = new Uint8Array(4); - crypto.getRandomValues(bytes); - return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); +/** The good_regions layer's box of each stored entry, or null for one that + * is not a region (the Python's region_boxes, by the rule the server's layer + * draws by), so the layer's ids stay the entries' indices. */ +export async function regionBoxes(infer: Pick, regions: readonly unknown[]): Promise<(RegionBox | null)[]> { + return regions.length ? infer.python<(RegionBox | null)[]>(`${VOLUME_CALLS}:region_boxes`, { regions }) : []; } -/** The default size of a region, in nm: one model output block of the - * volume (good_regions._default_size_nm: the output size times the output - * voxel size), which is what can be seen and judged on screen and what a - * training sample computes its loss over. */ -export function defaultRegionSizeNm(volume: Pick): number[] { - return volume.chunk_size.map((n, a) => n * volume.output_voxel_size[a]); +/** The region a mark makes (mark_current_view_good), by the Python's + * mark_region: a box of the size `sizeNm` asks for, as the request sent it + * (one model output block of `volume` when none is, 896 nm a side without a + * volume; one number for every axis, or three), centred on `centreNm`, + * labelled `label` or good- after the `stored` regions; with the + * layer's boxes of the stored regions and it. A size that is not one is + * answered as the sentence it is refused with. */ +export function markRegion( + infer: Pick, + stored: readonly unknown[], + centreNm: readonly number[], + sizeNm: unknown, + label: unknown, + volume: Pick | null, +): Promise<{ region: GoodRegion; boxes: (RegionBox | null)[] } | { error: string }> { + return infer.python(`${VOLUME_CALLS}:mark_region`, { + centre_nm: centreNm, + size_nm: sizeNm ?? null, + label: label ?? null, + regions: stored, + volume: volume && { chunk_size: volume.chunk_size, output_voxel_size: volume.output_voxel_size }, + }); } -/** The region a mark makes (mark_current_view_good): a box of `sizeNm` - * centred on `centreNm`, labelled `label` or good- after `count` - * regions. */ -export function regionAt(centreNm: readonly number[], sizeNm: readonly number[], count: number, label?: string, id: string = newRegionId()): GoodRegion { - return { - id, - label: label || `good-${count + 1}`, - offset_nm: centreNm.map((c, a) => c - sizeNm[a] / 2), - shape_nm: [...sizeNm], - }; -} +/** A stored entry that is a region: its box, and its id and label as stored + * (either may be missing: neither is needed to place a region). */ +export type StoredRegion = Pick & Partial>; /** A region's anchor: the box of the volume's voxels it holds to the * teacher, [lo, hi) per axis (z, y, x), clipped to the volume. */ export interface AnchorBox { - region: GoodRegion; + region: StoredRegion; lo: number[]; hi: number[]; } -/** Rounded to a millionth of a voxel before a floor or a ceiling: the float - * noise of nm → voxels (100.5 × 8 / 8 come back as 100.49999999999999) must - * not move a box by a voxel. */ -export const steady = (x: number) => Math.round(x * 1e6) / 1e6; - /** The anchor box of each usable region of `regions` on `volume`'s grid, and * what was skipped and why (sampler._index_rehearsal): an entry that is not - * a region, or a region whose centre lies outside the volume. - * - * The box is the server's rehearsal patch: one output block (the volume's - * chunk_size) whose centre is the region's centre snapped so that the block - * is whole voxels, `lo = floor(c - size/2 + 1/2)` with c the centre in - * voxels from the volume's corner (voxel v spans [v, v + 1)), then clipped - * to the volume. */ -export function anchorBoxes(volume: Pick, regions: readonly unknown[]): { boxes: AnchorBox[]; malformed: unknown[]; outside: GoodRegion[] } { - const corner = volumeCornerNm(volume.dataset_offset_nm, volume.output_voxel_size); - const shape = volume.dataset_shape_voxels, size = volume.chunk_size; - const boxes: AnchorBox[] = [], malformed: unknown[] = [], outside: GoodRegion[] = []; - for (const region of regions) { - if (!isGoodRegion(region)) { - malformed.push(region); - continue; - } - const centre = region.offset_nm.map((o, a) => (o + region.shape_nm[a] / 2 - corner[a]) / volume.output_voxel_size[a]); - if (centre.some((c, a) => c < 0 || c >= shape[a])) { - outside.push(region); - continue; - } - const start = centre.map((c, a) => Math.floor(steady(c - size[a] / 2 + 0.5))); - const lo = start.map((s) => Math.max(s, 0)), hi = start.map((s, a) => Math.min(s + size[a], shape[a])); - if (hi.some((h, a) => h <= lo[a])) { - outside.push(region); - continue; - } - boxes.push({ region, lo, hi }); - } - return { boxes, malformed, outside }; + * a region, or a region whose centre lies outside the volume. The Python's + * anchor_boxes: the server's rehearsal patch, one output block (the + * volume's chunk_size) whose start is the region's centre snapped so that + * the block is whole voxels, clipped to the volume. */ +export async function anchorBoxes( + infer: Pick, + volume: Pick, + regions: readonly unknown[], +): Promise<{ boxes: AnchorBox[]; malformed: unknown[]; outside: StoredRegion[] }> { + if (!regions.length) return { boxes: [], malformed: [], outside: [] }; + const grid = { chunk_size: volume.chunk_size, output_voxel_size: volume.output_voxel_size, dataset_offset_nm: volume.dataset_offset_nm, dataset_shape_voxels: volume.dataset_shape_voxels }; + // Each by its index in `regions`, which travel as they are stored. + const placed = await infer.python<{ boxes: { region: number; lo: number[]; hi: number[] }[]; malformed: number[]; outside: number[] }>(`${VOLUME_CALLS}:anchor_boxes`, { regions, volume: grid }); + return { + boxes: placed.boxes.map(({ region, lo, hi }) => ({ region: regions[region] as StoredRegion, lo, hi })), + malformed: placed.malformed.map((i) => regions[i]), + outside: placed.outside.map((i) => regions[i] as StoredRegion), + }; } /** The chunks the boxes reach, each with its voxels inside any box (a mask diff --git a/web/src/finetune/run.ts b/web/src/finetune/run.ts index 45bc528c..220a580f 100644 --- a/web/src/finetune/run.ts +++ b/web/src/finetune/run.ts @@ -19,7 +19,7 @@ import { runFull } from "./full/run-full"; import { count, epochSummary, plural } from "./log-words"; import { headOf, withHead } from "./onnx-head"; import { collectSamples } from "./samples"; -import { targetTransformFor } from "./targets"; +import { targetTransformFor } from "./target-call"; import type { FinetuneOptions, FinetuneRun, HeadFinetuned, RunFinetune } from "./types"; import { chunkKey, paintedChunkIndices } from "./volume"; @@ -65,9 +65,9 @@ export function createRunFinetune(deps: RunDeps = { collectSamples, targetTransf // good regions' unpainted voxels when the volume has any, whatever the // form's scope, as the server's anchors decide alone; else the form's. const lambda = options.distillationLambda; - const { boxes, malformed, outside } = anchorBoxes(volume, run.goodRegions ?? []); - for (const region of malformed) report.log(`Skipping a good region that is not one (an id, a label, offset_nm and shape_nm of three numbers): ${JSON.stringify(region)}`); - for (const region of outside) report.log(`Good region ${region.label} (${region.id}) centres outside the volume; skipping it`); + const { boxes, malformed, outside } = await anchorBoxes(run.infer, volume, run.goodRegions ?? []); + for (const region of malformed) report.log(`Skipping a good region that is not one (offset_nm and shape_nm of three numbers, the shape positive): ${JSON.stringify(region)}`); + for (const region of outside) report.log(`Good region ${region.label ?? "?"} (${region.id ?? "?"}) centres outside the volume; skipping it`); let pick: DistillationPick | null = null; let indices = paintedIndices; const paintedKeys = new Set(paintedIndices.map(chunkKey)); @@ -90,7 +90,7 @@ export function createRunFinetune(deps: RunDeps = { collectSamples, targetTransf const equalQuota = Math.ceil(DISTILLATION_VOXELS / Math.max(chunks, 1)); const random = mulberry32((options.seed ^ SAMPLE_STREAM) >>> 0); - const targets = deps.targetTransformFor(run.entry); + const targets = deps.targetTransformFor(run.entry, run.infer); const runner = await deps.createFeatureRunner({ bytes: run.baseBytes, head, inputName: run.entry.manifest.io.input, outputName: run.entry.manifest.io.output, log: (line) => report.log(line) }); try { const sets: PixelSet[] = [], heldSets: DistillationSet[] = []; @@ -99,7 +99,7 @@ export function createRunFinetune(deps: RunDeps = { collectSamples, targetTransf i++; if (report.cancelled()) throw new Error("cancelled before training"); const features = await runner.run(sample.input, sample.inputDims); - const chunkTargets = targets(sample.chunk); + const chunkTargets = await targets(sample.chunk); const set = gatherPixels(features, chunkTargets, head, sample.chunk.shape); if (paintedKeys.has(chunkKey(sample.chunk.index))) paintedChunks++; painted += set.count; diff --git a/web/src/finetune/samples.ts b/web/src/finetune/samples.ts index fbe40bfb..95f45bd5 100644 --- a/web/src/finetune/samples.ts +++ b/web/src/finetune/samples.ts @@ -27,6 +27,7 @@ // session's own input_norm_config: the paint was drawn over the prediction // that chain produced. import { inputBox, padded, type StageAt } from "../../chunkmirage/web/src/stages"; +import type { Inference } from "../contracts"; import { chooseRawLevel, geometryOf, halo as haloOf, inputShape, outputLevel, outputShape, ratio } from "../infer/geometry"; import { modelInput } from "../infer/stages"; import type { FinetuneRun, PaintedChunk, Sample } from "./types"; @@ -43,7 +44,7 @@ import { paintedChunkIndices, readPaintedChunk } from "./volume"; * painted among them is a sample all the same, its labels all 0: a good * region's block anchors the model whether or not anything in it was * painted (run.ts). */ -export async function* collectSamples(run: Pick, indices?: number[][]): AsyncGenerator { +export async function* collectSamples(run: Pick & { infer: Pick }, indices?: number[][]): AsyncGenerator { const { volume, dataset, entry } = run; // The grid the model is served on, as engine.ts's serve() finds it: the // raw level the model reads, and the output level over it. diff --git a/web/src/finetune/target-call.ts b/web/src/finetune/target-call.ts new file mode 100644 index 00000000..fc54e20c --- /dev/null +++ b/web/src/finetune/target-call.ts @@ -0,0 +1,61 @@ +// A painted chunk's labels as training targets, made by the Python both +// modes run: cellmap_flow/portable/targets.py, the functions the server's +// finetune/target_transforms.py wraps for its trainer, called here in the +// engine's Pyodide worker through Inference.python +// (portable/targets_calls.py decodes the labels and encodes the answer). So +// the head's trainer, the whole network's and the ranking train and score +// on the targets the server's finetune would make from the same paint; +// there is no port to keep in step. +// +// Which target a model takes, and an affinity model's offsets, the Python +// decides too, from the model's channel names (label_it.affinity_offsets, +// the server's rule in autodetect_output_type: a model with a channel named +// `*_aff*` is an affinity model, its first affinity channels along z, y and +// x; every other model is a binary one, the one target broadcast to all its +// channels). The server takes offsets from a model's script when it has +// one; the browser has no script, and the manifests on Hugging Face say no +// offsets (none of the seven did on 2026-10-21), so the names are all it +// has, read by the one rule. The channels past the offsets (LSDs, say) +// train nothing in both modes. +// +// A chunk is one sample without the batch axis: its labels are its voxels +// in C order (z, y, x), and a target or mask is `channels × N` values with +// the channel outermost, so channel c's voxel i is at c * N + i, the +// server's (0, c, z, y, x) once the batch is gone. +import type { Inference, ModelEntry } from "../contracts"; +import type { PaintedChunk, Targets, TargetTransform } from "./types"; + +/** The portable Python's entry point for targets, by module path and name. */ +const TARGETS = "cellmap_flow.portable.targets_calls:targets"; + +/** The transform a model's labels train it with, each chunk's targets made + * by `infer`'s Python from the model's channel names: affinities for a + * model with affinity channels, the binary target on every channel + * otherwise. */ +export function targetTransformFor(entry: ModelEntry, infer: Pick): TargetTransform { + const channels = entry.manifest.channels; + if (!channels.length) throw new Error(`${entry.name}'s manifest names no output channels to train`); + const C = channels.length; + return async (chunk: PaintedChunk): Promise => { + const n = voxelsOf(chunk); + const labels_dtype = chunk.labels instanceof Uint16Array ? "uint16" : "uint8"; + const { arrays } = await infer.python<{ value: { channels: number }; arrays: ArrayBuffer[] }>( + TARGETS, { shape: chunk.shape, labels_dtype, channels: C, channel_names: channels }, [bytesOf(chunk.labels)]); + const target = new Float32Array(arrays[0]), mask = new Float32Array(arrays[1]); + if (target.length !== C * n || mask.length !== C * n) throw new Error(`the Python's targets have ${target.length} and ${mask.length} values for ${C} channel(s) of ${n} voxels`); + return { channels: C, target, mask }; + }; +} + +/** The chunk's voxel count, checked against its labels. */ +function voxelsOf(chunk: PaintedChunk): number { + if (chunk.shape.length !== 3) throw new Error(`a chunk has three axes (z, y, x), not ${chunk.shape.length}`); + const n = chunk.shape.reduce((a, b) => a * b, 1); + if (chunk.labels.length !== n) throw new Error(`a chunk of ${chunk.shape.join("×")} has ${n} voxels, but ${chunk.labels.length} labels`); + return n; +} + +/** A typed array's own bytes, copied: a call's arrays are handed to the worker. */ +function bytesOf(a: ArrayBufferView): ArrayBuffer { + return a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength) as ArrayBuffer; +} diff --git a/web/src/finetune/targets.ts b/web/src/finetune/targets.ts deleted file mode 100644 index 32387f3d..00000000 --- a/web/src/finetune/targets.ts +++ /dev/null @@ -1,135 +0,0 @@ -// A painted chunk's labels as training targets: the port of -// cellmap_flow/finetune/target_transforms.py (BinaryTargetTransform, -// BroadcastBinaryTargetTransform, AffinityTargetTransform with its -// _offset_slices) for the browser, checked against the Python classes by -// web/test/parity/targets.test.ts. -// -// The Python works on a batch, (B, 1, Z, Y, X) in and (B, C, Z, Y, X) out; -// a chunk here is one sample without the batch axis: its labels are the -// chunk's voxels in C order (z, y, x), and a target or mask is `channels × N` -// values with the channel outermost, so channel c's voxel i is at c * N + i, -// which is the same memory order as the Python's (0, c, z, y, x) once the -// batch is gone. Labels mean what the paint layer paints: 0 unpainted (no -// loss), 1 background, 2 and up objects. -// -// Which transform a model gets (targetTransformFor) follows the channels its -// manifest names, as the server's autodetect_output_type (dashboard/routes/ -// finetune/common.py) follows them when nothing else says: a model with a -// channel named `*_aff*` is an affinity model (volume.ts hasAffinityChannels, -// the same test that gives its volume uint16 ids), and every other model is -// a binary one, the one target broadcast to all its channels. The server -// takes an affinity model's offsets from its script, or the unit offsets in -// z, y, x order by default; here the channel names say them (`mito_aff_z`, -// `mito_aff_y`, `mito_aff_x`, which is how Marwan's exports name them), and -// a channel of such a model that names no offset is masked out, as the -// server masks the channels past its offsets (LSDs, say). -import type { ModelEntry } from "../contracts"; -import type { PaintedChunk, Targets, TargetTransform } from "./types"; -import { hasAffinityChannels } from "./volume"; - -const prod = (shape: readonly number[]) => shape.reduce((a, b) => a * b, 1); - -/** BinaryTargetTransform for one channel, BroadcastBinaryTargetTransform for - * more: target 1 where the voxel is an object (label 2 or more, the Python's - * `clamp(label - 1, min=0) > 0`), mask 1 where it is painted (label > 0), - * the same on every channel. */ -export function binaryTargets(channels: number): TargetTransform { - if (!Number.isInteger(channels) || channels < 1) throw new Error(`a binary target needs at least one channel, not ${channels}`); - return (chunk: PaintedChunk): Targets => { - const n = voxelsOf(chunk); - const target = new Float32Array(channels * n), mask = new Float32Array(channels * n); - const { labels } = chunk; - for (let i = 0; i < n; i++) { - const label = labels[i]; - if (label > 1) target[i] = 1; - if (label > 0) mask[i] = 1; - } - // expand(...).contiguous(): the first channel's values copied to the others. - for (let c = 1; c < channels; c++) { - target.copyWithin(c * n, 0, n); - mask.copyWithin(c * n, 0, n); - } - return { channels, target, mask }; - }; -} - -/** AffinityTargetTransform: channel i is the affinity along `offsets[i]`, - * 1 where a voxel and its neighbour at the offset are the same object - * (equal labels above 1), masked where both are painted; the pair is - * written at the voxel (the Python's `src_slices`), and a voxel whose - * neighbour falls outside the chunk is left unmasked, as are the channels - * past the offsets (`num_channels` beyond `len(offsets)`: LSDs, say, which - * then get no gradient) and any channel whose offset is null (one named no - * offset, affinityOffsets). An offset no smaller than the chunk along its - * axis pairs nothing: a one-slice chunk's z channel has an empty mask, - * which is right for a 2D student (its z head keeps the model's weights). */ -export function affinityTargets(offsets: (number[] | null)[], channels: number): TargetTransform { - if (!Number.isInteger(channels) || channels < 1) throw new Error(`an affinity target needs at least one channel, not ${channels}`); - if (offsets.length > channels) throw new Error(`${offsets.length} affinity offsets for ${channels} channel(s): a model has a channel per offset`); - for (const offset of offsets) { - if (offset && (offset.length !== 3 || offset.some((d) => !Number.isInteger(d)))) throw new Error(`an affinity offset is three whole numbers (dz, dy, dx), not ${JSON.stringify(offset)}`); - } - return (chunk: PaintedChunk): Targets => { - const n = voxelsOf(chunk); - const [sz, sy, sx] = chunk.shape; - const target = new Float32Array(channels * n), mask = new Float32Array(channels * n); - const { labels } = chunk; - offsets.forEach((offset, c) => { - if (!offset) return; // a channel with no offset: masked out - const [dz, dy, dx] = offset; - // _offset_slices: the source voxels are those whose neighbour at the - // offset is in the chunk, [max(0, -d), min(size, size - d)) along each - // axis; the neighbour is d further on. Empty when |d| >= size. - const z0 = Math.max(0, -dz), z1 = Math.min(sz, sz - dz); - const y0 = Math.max(0, -dy), y1 = Math.min(sy, sy - dy); - const x0 = Math.max(0, -dx), x1 = Math.min(sx, sx - dx); - const step = (dz * sy + dy) * sx + dx; // the neighbour's index minus the voxel's - const base = c * n; - for (let z = z0; z < z1; z++) for (let y = y0; y < y1; y++) { - const row = (z * sy + y) * sx; - for (let x = x0; x < x1; x++) { - const i = row + x, src = labels[i], dst = labels[i + step]; - if (src === dst && src > 1) target[base + i] = 1; - if (src > 0 && dst > 0) mask[base + i] = 1; - } - } - }); - return { channels, target, mask }; - }; -} - -/** The affinity offset of each of `channelNames`, in the channels' order: - * the unit offset a name ending in `_aff_z`, `_aff_y` or `_aff_x` says - * (mito_aff_z, mito_aff_y, mito_aff_x → (1,0,0), (0,1,0), (0,0,1)), and - * null for a channel named no offset, which the affinity target masks out. */ -export function affinityOffsets(channelNames: string[]): (number[] | null)[] { - return channelNames.map((name) => { - const axis = /_aff_(z|y|x)$/.exec(name)?.[1]; - return axis ? UNIT_OFFSETS[axis] : null; - }); -} - -const UNIT_OFFSETS: Record = { z: [1, 0, 0], y: [0, 1, 0], x: [0, 0, 1] }; - -/** The transform a model's labels train it with: affinities along the - * offsets its channel names say for a model with affinity channels - * (hasAffinityChannels, the server's test), its other channels masked - * out; the binary target on every channel otherwise. */ -export function targetTransformFor(entry: ModelEntry): TargetTransform { - const channels = entry.manifest.channels; - if (!channels.length) throw new Error(`${entry.name}'s manifest names no output channels to train`); - if (!hasAffinityChannels(entry)) return binaryTargets(channels.length); - const offsets = affinityOffsets(channels); - if (offsets.every((offset) => offset === null)) { - throw new Error(`${entry.name}'s channels (${channels.join(", ")}) are affinities, but none is named _aff_z, _aff_y or _aff_x, so their offsets are unknown`); - } - return affinityTargets(offsets, channels.length); -} - -/** The chunk's voxel count, checked against its labels. */ -function voxelsOf(chunk: PaintedChunk): number { - if (chunk.shape.length !== 3) throw new Error(`a chunk has three axes (z, y, x), not ${chunk.shape.length}`); - const n = prod(chunk.shape); - if (chunk.labels.length !== n) throw new Error(`a chunk of ${chunk.shape.join("×")} has ${n} voxels, but ${chunk.labels.length} labels`); - return n; -} diff --git a/web/src/finetune/types.ts b/web/src/finetune/types.ts index 12259862..85a26ff0 100644 --- a/web/src/finetune/types.ts +++ b/web/src/finetune/types.ts @@ -7,8 +7,9 @@ // samples.ts the painted chunks of a volume and, for each, the // model's input block around it, read and normalized as // the model stage feeds the model (a Sample); -// targets.ts a chunk's labels as training targets per output channel -// with the voxels the loss counts (TargetTransform); +// target-call.ts a chunk's labels as training targets per output channel +// with the voxels the loss counts (TargetTransform), made +// by cellmap_flow/portable/targets.py in the engine's Python; // onnx-head.ts the model's head, the last 1×1 convolution, found in // the ONNX graph; the graph with the head's input tensor // as a second output; the graph with new head weights; @@ -76,7 +77,8 @@ export interface Targets { mask: Float32Array; } -export type TargetTransform = (chunk: PaintedChunk) => Targets; +/** A chunk's targets, answered by the engine's Python (target-call.ts). */ +export type TargetTransform = (chunk: PaintedChunk) => Promise; /** A model's head: the last 1×1 convolution of its ONNX graph, which makes * the output channels from the final feature map, and its weights as @@ -227,14 +229,15 @@ export interface GoodRegion { * job started from, whichever head the run starts from (run.ts). The good * regions are the entries of the volume's good_regions.json as they are * when the run starts (good-regions.ts loadGoodRegions); one that is not a - * GoodRegion is skipped with a line in the log, as the server skips it. */ + * region is skipped with a line in the log, as the server skips it. */ export interface FinetuneRun { entry: ModelEntry; baseBytes: ArrayBuffer; volume: PaintVolume; chain: Chain; dataset: DatasetInfo; - infer: Pick; + /** The raw's reads, and the portable Python (the good regions' anchors). */ + infer: Pick; paint: PaintStore; options: FinetuneOptions; report: RunReporter; diff --git a/web/src/finetune/volume.ts b/web/src/finetune/volume.ts index f0ac449d..14301ea0 100644 --- a/web/src/finetune/volume.ts +++ b/web/src/finetune/volume.ts @@ -1,15 +1,14 @@ // An annotation volume in the browser: planned over the open dataset for a -// model (planVolume), written as zarr v2 files into the paint store -// (volumeFiles), and read back for training (readVolume, the painted chunks). -// The port of cellmap_flow/finetune/session/volume.py (plan_volume, -// create_volume_zarr, read_volume) for the browser, where the files are the -// paint store's and the grid is the one the model stage serves on -// (infer/geometry.ts); the labels' dtype follows -// cellmap_flow/dashboard/routes/finetune/common.py's annotation_dtype_for. -// See PLAN_browser_finetune.md, "The volume's layout". -import type { Chain, DatasetInfo, ModelEntry, PaintStore, PaintVolume } from "../contracts"; -import { chooseRawLevel, geometryOf, inputShape, outputLevel, outputShape } from "../infer/geometry"; -import { isSegmentation } from "../infer/post"; +// model (planVolume, by cellmap_flow/portable/volume.py, the Python the +// server plans with, run in the engine's Pyodide worker), written as zarr v2 +// files into the paint store (volumeFiles), and read back for training +// (readVolume, the painted chunks). This file reads what the plan is made +// from (the raw level the model reads, the model's blocks) and writes and +// reads the files; the plan, the grid and the labels' dtype, is the +// Python's. See PLAN_browser_finetune.md, "The volume's layout". +import { volumeCornerNm } from "../ai-annotate/fill"; +import type { Chain, DatasetInfo, Inference, ModelEntry, PaintStore, PaintVolume } from "../contracts"; +import { chooseRawLevel, geometryOf } from "../infer/geometry"; import { stamp } from "../shared/format"; import type { PaintedChunk } from "./types"; @@ -42,81 +41,53 @@ function randomHex(n: number): string { // Planning a volume // --------------------------------------------------------------------------- -/** A new volume over `dataset` for `entry`, through `chain`: plan_volume, - * on the grid the served prediction has. - * - * The server snaps each of the model's voxel sizes to the raw level closest - * to it (closest_raw_scale) and lays the volume on the output level's grid - * from the data's corner, padded to whole chunks. Here the grid is the one - * engine.ts's serve() builds for the model's view, so a chunk of paint is - * exactly a chunk of the prediction layer it is painted over: the raw level - * chooseRawLevel picks for the model's input voxel size, read as if it were - * at that size, and the output level outputLevel derives from it (its voxel - * size the raw level's times the model's ratio, its shape the raw level's - * floor-divided by the ratio, its origin the raw level's corner plus half an - * output voxel). DatasetInfo's levels are in nm (datasetInfoFrom), so the - * volume is in nm, where serve() gives outputLevel the engine's level in the - * source's own units for the view. input_voxel_size is the raw level's, as - * the server's is the closest raw level's; the model's own are kept as the - * claimed sizes, for provenance. - * - * dataset_offset_nm is the output level's origin. chunkmirage reads a - * level's OME translation as its origin (web/chunkmirage/web/src/ome.ts, - * openImage), and a translation is voxel 0's centre, which is what the - * server stores and writes back as the volume's own translation (volume.py's - * module docstring, volume_corner_nm), so the paint is drawn where the - * prediction is. The extent is the level's shape rounded up to whole chunks, - * as the server's _grid pads it, so that the last chunk along each axis is a - * whole sample. +/** The portable Python's entry points for volumes and good regions (cellmap_flow/portable/volume_calls.py), by module path. */ +export const VOLUME_CALLS = "cellmap_flow.portable.volume_calls"; + +/** What plan_volume answers when it plans: the plan's fields of a PaintVolume. */ +type Plan = Pick; + +/** A new volume over `dataset` for `entry`, through `chain`: the server's + * plan_volume, which is cellmap_flow/portable/volume.py's, called in the + * engine's Pyodide worker (`infer.python`). What it is given is what the + * browser read: the raw level the model reads (chooseRawLevel, the server's + * "floor" level choice, as engine.ts's serve() picks it), its voxel-0 + * corner, and the model's voxel sizes and blocks (geometryOf). The Python + * answers the grid, the one the model's prediction is served on (the + * level's corner, its voxel times the model's ratio: outputLevel's + * arithmetic, so a chunk of paint is a chunk of the prediction layer it is + * painted over), the extent (the level's, rounded up to whole output voxels + * and padded to whole chunks) and the labels' dtype (uint16 for a model + * trained on instances, here an affinity model: hasAffinityChannels, as the + * browser trains it). A refusal comes back as its sentence, thrown here. * * Refused for a dataset with more than one channel, as serve() refuses it: * the browser mode reads single-channel volumes. */ -export function planVolume(dataset: DatasetInfo, entry: ModelEntry, chain: Chain, options: { id?: string; now?: Date } = {}): PaintVolume { +export async function planVolume(infer: Pick, dataset: DatasetInfo, entry: ModelEntry, chain: Chain, options: { id?: string; now?: Date } = {}): Promise { if (dataset.channels > 1) { throw new Error(`${dataset.url} has ${dataset.channels} channels; the browser mode paints and trains on single-channel volumes (it does not select a channel yet)`); } - const g = geometryOf(entry); - const raw = dataset.levels[chooseRawLevel(dataset.levels, g.input_voxel_size)]; - const level = outputLevel(raw, g); - const chunk = outputShape(g); + const model = geometryOf(entry); + const raw = dataset.levels[chooseRawLevel(dataset.levels, model.input_voxel_size)]; + const plan = await infer.python(`${VOLUME_CALLS}:plan_volume`, { + level: { shape: raw.shape, voxel: raw.voxel, corner: volumeCornerNm(raw.origin, raw.voxel) }, + model, + output_type: hasAffinityChannels(entry) ? "affinities" : "binary", + }); + if ("error" in plan) throw new Error(plan.error); return { id: options.id ?? newVolumeId(options.now), type: "annotation_volume", model_name: entry.name, model_id: entry.id, dataset_path: dataset.url, - chunk_size: chunk, - input_size: inputShape(g), - output_voxel_size: level.voxel, - input_voxel_size: [...raw.voxel], - claimed_output_voxel_size: g.output_voxel_size, - claimed_input_voxel_size: g.input_voxel_size, - dataset_offset_nm: level.origin, - dataset_shape_voxels: level.shape.map((n, a) => Math.ceil(n / chunk[a]) * chunk[a]), - dtype: annotationDtypeFor(entry, chain), + ...plan, input_norm: chain.input_norm.map((step) => ({ ...step })), postprocess: chain.postprocess.map((step) => ({ ...step })), created_at: (options.now ?? new Date()).toISOString(), }; } -/** The labels' dtype of a new volume for `entry` through `chain`: common.py's - * annotation_dtype_for. The server asks what the model trains on - * (autodetect_output_type) and gives uint16 to a target trained on - * instances, whose paint numbers each object (282 of a Cellpose model's - * masks in one box did not fit a uint8 volume): affinities, which, when the - * model type declares nothing, it tells by a channel name containing "_aff"; - * and Cellpose's flows, which only a model type declares. Everything else is - * uint8, background and foreground. The channel-name test is followed as it - * stands (hasAffinityChannels); the browser has no model types or scripts - * to ask, and no flow model is on Hugging Face. One rule is the browser's - * own: a chain that serves labels (isSegmentation: a threshold, an affinity - * segmentation) is painted over object by object, each with its id, so its - * volume holds ids too. */ -export function annotationDtypeFor(entry: ModelEntry, chain: Chain): PaintVolume["dtype"] { - return hasAffinityChannels(entry) || isSegmentation(chain.postprocess) === true ? "uint16" : "uint8"; -} - /** Whether a model's channels are affinities (`mito_aff_z`, `mito_aff_y`, * `mito_aff_x`), by the server's test: a channel name containing "_aff" * (common.py's autodetect_output_type). */ diff --git a/web/src/infer/geometry.ts b/web/src/infer/geometry.ts index d383be53..16ad2014 100644 --- a/web/src/infer/geometry.ts +++ b/web/src/infer/geometry.ts @@ -153,7 +153,10 @@ export function chooseRawLevel(levels: Level[], voxel: number[]): number { * at the raw level's corner (chunkmirage_server.py: "output voxel 0 starts * at the corner of the level the model reads"), so the served origin is that * corner plus half an output voxel. With equal voxel sizes it is the raw - * origin. */ + * origin. An annotation volume lies on this grid: the portable Python + * plans it (cellmap_flow/portable/volume.py plan_volume) with the same + * arithmetic in the same order, so its voxel size and origin are these to + * the last bit and a chunk of paint is a block of this level. */ export function outputLevel(raw: Level, g: BlockGeometry): Level { const voxel = raw.voxel.map((v, a) => v * ratio(g)[a]); return { @@ -202,6 +205,11 @@ export function datasetInfoFrom(url: string, src: SourceInfo): DatasetInfo { }; } +/** Rounded to a millionth of a voxel before a floor or a ceiling: the float + * noise of nm → voxels (100.5 × 8 / 8 come back as 100.49999999999999) must + * not move a box by a voxel. */ +export const steady = (x: number) => Math.round(x * 1e6) / 1e6; + function wholeVoxels(value: number, what: string): number { const n = Math.round(value); if (Math.abs(value - n) > 1e-6) throw new Error(`${what} (${value} voxels) is not whole voxels`); diff --git a/web/src/label-it/prediction.ts b/web/src/label-it/prediction.ts index 1ae138bc..9db51dba 100644 --- a/web/src/label-it/prediction.ts +++ b/web/src/label-it/prediction.ts @@ -1,26 +1,20 @@ -// The prediction under a box of the annotation volume: the port of -// cellmap_flow/dashboard/routes/finetune/view_labels.py's read_prediction -// for the browser, where the served model's output is the page's own. +// The prediction under a box of the annotation volume, in the browser: where +// it is read and the reading, around the Python both modes run +// (cellmap_flow/portable/label_it.py's placement, through label_it_calls.py +// in the engine's Pyodide worker), which says which served voxel lies under +// each annotation voxel. The server reads its model server's array the same +// way (view_labels.read_prediction). // -// The server reads its model server's array with the dashboard's input -// normalization and no postprocessing: the trainer's target is set on the -// raw output, and that is where the model's decision boundary is. Here the -// same output is Inference.readOutput's, the model stage's (the chain's -// normalizers, none of its postprocessors), from the engine's caches when -// the viewer has had those chunks computed, so a seed copies the prediction -// the user is looking at without running the model again. -// -// Where the two grids meet: each annotation voxel takes the served voxel its -// centre lies in, so a seed from a model served at another voxel size (any -// served model can seed, not only the one the volume is for) lies under that -// model's layer. Both grids are placed by their OME translation, voxel 0's -// centre (the volume's dataset_offset_nm, the served level's origin), so a -// grid's corner is half a voxel before it. The box shrinks to what the -// served level covers; a box it does not reach at all is refused. +// The served model's output is the page's own here: Inference.readOutput's, +// the model stage's (the chain's normalizers, none of its postprocessors, as +// the server reads its model server's array with no postprocessing: the +// trainer's target is set on the raw output, and that is where the model's +// decision boundary is), from the engine's caches when the viewer has had +// those chunks computed, so a seed copies the prediction the user is looking +// at without running the model again. import type { DatasetInfo, Inference, ModelEntry, PaintVolume } from "../contracts"; -import { volumeCornerNm } from "../ai-annotate/fill"; import { chooseRawLevel, geometryOf, type Level, outputLevel } from "../infer/geometry"; -import type { Block } from "./segment"; +import { type Block, LABEL_IT } from "./segment"; /** The level a model's output is served at over `dataset`, in nm: the output * grid infer/engine.ts serves the model's view on, from the raw level @@ -32,85 +26,55 @@ export function servedLevel(dataset: DatasetInfo, entry: ModelEntry): Level { return outputLevel(dataset.levels[chooseRawLevel(dataset.levels, g.input_voxel_size)], g); } -/** Which served voxels the annotation voxels [lo, hi) read: `indices[a]`, - * per axis, the served voxel each annotation voxel's centre lies in, for the - * voxels the level covers, which are [lo, hi) of the answer (the box shrunk - * to them). null when the level covers no voxel of the box along some axis - * (read_prediction's "The view is outside what the model's server - * predicts."). */ -export function servedIndices( +/** Which served voxels the annotation voxels of a box read + * (label_it.placement): `indices[a]`, per axis, the served voxel each + * annotation voxel's centre lies in, for the voxels the level covers, which + * are [lo, hi) (the box shrunk to them); and [read_lo, read_hi), the box of + * the level those indices span, the one to read. */ +export interface Placement { + lo: number[]; + hi: number[]; + indices: number[][]; + read_lo: number[]; + read_hi: number[]; +} + +/** The box and the two grids as label_it_calls' placement takes them: the + * annotation voxels [lo, hi) of `volume`, on the served `level` (its origin + * is its OME translation, voxel 0's centre, as the volume's + * dataset_offset_nm is). */ +export function placementArgs(volume: Pick, lo: readonly number[], hi: readonly number[], level: Level) { + return { + lo: [...lo], + hi: [...hi], + volume: { output_voxel_size: volume.output_voxel_size, dataset_offset_nm: volume.dataset_offset_nm }, + level: { origin: level.origin, voxel: level.voxel, shape: level.shape }, + }; +} + +/** Where the annotation voxels [lo, hi) of `volume` lie on `level` + * (placementArgs), by the Python; null when the level covers no voxel of the + * box along some axis. */ +export function place( + infer: Pick, volume: Pick, lo: readonly number[], hi: readonly number[], level: Level, -): { lo: number[]; hi: number[]; indices: number[][] } | null { - const voxel = volume.output_voxel_size; - const corner = volumeCornerNm(volume.dataset_offset_nm, voxel); - // The server's corner of the served level (ome_corner of its translation) - // rescaled from the voxel size declared to the one Neuroglancer draws it - // at, `corner / declared * drawn`; the browser draws a layer at the size - // it declares, and the same arithmetic keeps the floor below on the same - // side of a voxel boundary as the Python's. - const servedCorner = level.origin.map((o, a) => ((o - level.voxel[a] / 2) / level.voxel[a]) * level.voxel[a]); - const newLo = [...lo], newHi = [...hi], indices: number[][] = []; - for (let a = 0; a < 3; a++) { - const inside: number[] = []; - let first = -1, last = -1; - for (let i = lo[a]; i < hi[a]; i++) { - const centre = corner[a] + (i + 0.5) * voxel[a]; - const index = Math.floor((centre - servedCorner[a]) / level.voxel[a]); - if (index < 0 || index >= level.shape[a]) continue; - if (first < 0) first = i; - last = i; - inside.push(index); - } - if (!inside.length) return null; - // The voxels inside are one run: the centres rise along the axis. - newLo[a] = first; - newHi[a] = last + 1; - indices.push(inside); - } - return { lo: newLo, hi: newHi, indices }; +): Promise { + return infer.python(`${LABEL_IT}:place`, placementArgs(volume, lo, hi, level)); } -/** The model's raw output over annotation voxels [lo, hi), as - * read_prediction answers it: `prediction` [C, z, y, x] float32 over the - * box shrunk to what the served level covers, and that box. `view` is the - * served model's (ServedModel.view) and `level` its served level - * (servedLevel). null when the level covers nothing of the box. */ -export async function readPrediction( - infer: Pick, - view: string, - level: Level, - volume: Pick, - lo: readonly number[], - hi: readonly number[], -): Promise<{ prediction: Block; lo: number[]; hi: number[] } | null> { - const placed = servedIndices(volume, lo, hi, level); - if (!placed) return null; - const { indices } = placed; - // The served box the indices span, read once, then each annotation voxel - // its served voxel (np.ix_ in the Python); with the two grids equal, the - // whole region in order. - const readLo = indices.map((ix) => ix[0]); - const readHi = indices.map((ix) => ix[ix.length - 1] + 1); - const region = await infer.readOutput(view, readLo, readHi); - const [C, rz, ry, rx] = region.shape; - const span = readHi.map((h, a) => h - readLo[a]); +/** The served model's raw output over the box `placement` says to read: + * [C, z, y, x] float32, for the Python to put on the annotation grid + * (label_it.gather, in label_it_calls.seed). `view` is the served model's + * (ServedModel.view). */ +export async function readRegion(infer: Pick, view: string, placement: Placement): Promise { + const region = await infer.readOutput(view, placement.read_lo, placement.read_hi); + const span = placement.read_hi.map((h, a) => h - placement.read_lo[a]); + const [, rz, ry, rx] = region.shape; if (region.shape.length !== 4 || rz !== span[0] || ry !== span[1] || rx !== span[2]) { throw new Error(`asked for ${span.join("×")} voxels of the model's output, got a block of ${region.shape.join("×")}`); } - const [iz, iy, ix] = indices.map((idx, a) => idx.map((v) => v - readLo[a])); - const shape = [C, iz.length, iy.length, ix.length]; - const data = new Float32Array(shape.reduce((a, b) => a * b, 1)); - let k = 0; - for (let c = 0; c < C; c++) { - for (const z of iz) { - for (const y of iy) { - const row = ((c * rz + z) * ry + y) * rx; - for (const x of ix) data[k++] = region.data[row + x]; - } - } - } - return { prediction: { data, shape }, lo: placed.lo, hi: placed.hi }; + return region; } diff --git a/web/src/label-it/segment.ts b/web/src/label-it/segment.ts index 36d3ca36..e96be96d 100644 --- a/web/src/label-it/segment.ts +++ b/web/src/label-it/segment.ts @@ -1,98 +1,21 @@ -// Seed from Prediction's segmentation: what the Finetune tab's "Label it" -// tools make of a model's prediction over the box on screen. The port of -// cellmap_flow/dashboard/routes/finetune/view_labels.py's probability, -// foreground_mask, _seed_plan, methods_for and segment_prediction for the -// browser. The segmenters themselves are already ported and parity-tested: -// connected components with scipy.ndimage.label's numbering -// (infer/label.ts, LabelPostprocessor's) and the mutex watershed with -// mwatershed.agglom's (infer/affinity.ts, AffinityPostprocessor's), so a -// seed's objects are the ones the server's seed makes of the same output. +// Seed from Prediction's segmentation in the browser is the Python both modes +// run: cellmap_flow/portable/label_it.py (the request's options, the +// channels a seed reads, the probability and the components method), called +// through label_it_calls.py in the engine's Pyodide worker +// (Inference.python). backend/routes/view-labels.ts makes those calls; this +// file holds the one step the Python cannot take here, and the probability +// the rank reads. // -// The threshold is a probability whatever the model's activation (the -// server's rule): the raw output is turned into one by its output class -// (model_info's output_class, serving/probe.py's vocabulary), so 0.5 is the -// model's own decision boundary for every model and the affinity channels -// can be averaged. The arithmetic follows numpy's float32 where the server's -// runs in float32 (scipy's expit in float32 steps, the channel mean summed -// channel by channel and divided once, the threshold compared as a float32), -// so a voxel is foreground here when it is there; expit can still differ by -// one float32 ulp from glibc's expf on about 3 in 10,000 values, which moves -// a voxel only when its probability is within that ulp of the threshold. -// -// What the browser leaves out: "distance_watershed" (segment.py's watershed -// of a distance map grown from its h-maxima) and "instances" (a model that -// serves integer ids, Cellpose's masks). The browser's models are those of -// Hugging Face's cellmap/onnx-web-models: affinity models (channels -// mito_aff_z/y/x) and binary ones (nuc, er), all raw logits. None outputs a -// distance or instance ids, so methodsFor never offers either, and a request -// for one is refused as the server refuses a method the model does not -// offer (the page lists them greyed out, saying what they need). -import type { ModelEntry } from "../contracts"; -import { affinityOffsets } from "../finetune/targets"; -import { hasAffinityChannels } from "../finetune/volume"; +// That step is the mutex watershed. mwatershed is Rust, with no Pyodide +// wheel, so for an affinity model the Python answers the watershed's input +// (label_it.affinities: the affinity channels as probabilities) and +// infer/affinity.ts's mutexWatershed, parity-tested against the server's +// segment.mutex_watershed (test/parity/label-it.test.ts), joins them here. +import type { Inference } from "../contracts"; import { mutexWatershed } from "../infer/affinity"; -import { connectedComponents } from "../infer/label"; - -/** How a seed makes objects of the prediction (view_labels.SEED_METHODS), the - * server's four, so a request names any of them as it would there; which a - * model offers is methodsFor's. */ -export const SEED_METHODS = ["components", "mutex_watershed", "distance_watershed", "instances"] as const; -export type SeedMethod = (typeof SEED_METHODS)[number]; - -/** What a model is trained on, as the browser tells it (the server's - * autodetect_output_type, by its channel-name test): "affinities" for a - * model with a channel named `*_aff*`, else "binary". */ -export type OutputType = "affinities" | "binary"; - -/** serving/probe.py's activation classes. */ -export const UNIT = "unit"; -export const SIGNED_UNIT = "signed_unit"; -export const UNBOUNDED = "unbounded"; -export function outputTypeOf(entry: ModelEntry): OutputType { - return hasAffinityChannels(entry) ? "affinities" : "binary"; -} - -/** The seed methods that fit a model's output, the best fit first - * (view_labels.methods_for): the mutex watershed for affinities, then - * components, which fits every output and so is always offered, last. The - * server's distance_watershed and instances fit outputs no browser model has - * (see the top of this file). */ -export function methodsFor(outputType: OutputType): SeedMethod[] { - return outputType === "affinities" ? ["mutex_watershed", "components"] : ["components"]; -} - -/** What a seed reads of a model (view_labels._seed_plan): `channels`, the - * output channels it thresholds (null for every one), and, for an affinity - * model, the `offsets` of its affinity channels. As the trainer targets - * them: the selected channel when the request names one; else an affinity - * model's offset channels (the rest, LSDs say, are masked out of its loss); - * else every channel, which a binary target is broadcast over. An affinity - * model's offsets are the ones its channel names say (finetune/targets.ts - * affinityOffsets: mito_aff_z, _y, _x are (1,0,0), (0,1,0), (0,0,1), the - * unit offsets the server gives the first three `_aff` channels in order). */ -export interface SeedPlan { - outputType: OutputType; - channels: number[] | null; - offsets: number[][] | null; -} - -export function seedPlan(entry: ModelEntry, selectChannel: number | null): SeedPlan { - const outputType = outputTypeOf(entry); - const named = outputType === "affinities" ? affinityOffsets(entry.manifest.channels) : []; - const offsetChannels = named.flatMap((offset, c) => (offset ? [c] : [])); - const offsets = offsetChannels.length ? offsetChannels.map((c) => named[c]!) : null; - if (selectChannel !== null) { - if (!Number.isInteger(selectChannel) || selectChannel < 0) throw new Error(`select_channel must be 0 or more, got ${selectChannel}`); - return { outputType, channels: [selectChannel], offsets }; - } - if (outputType === "affinities" && offsets) return { outputType, channels: offsetChannels, offsets }; - return { outputType, channels: null, offsets }; -} - -// --------------------------------------------------------------------------- -// The prediction as a probability -// --------------------------------------------------------------------------- +/** The Python's entry points for Label it, by module path. */ +export const LABEL_IT = "cellmap_flow.portable.label_it_calls"; /** A block of values, channels first: `shape` is [C, z, y, x]. */ export interface Block { @@ -100,142 +23,30 @@ export interface Block { shape: number[]; } -const prod = (shape: readonly number[]) => shape.reduce((a, b) => a * b, 1); - -/** serving/probe.py's classify_output_range: the activation class of an - * observed (min, max), with its small tolerance for a saturating activation - * landing a hair outside its asymptote in float32. */ -export function classifyOutputRange(lo: number, hi: number): string { - const eps = 1e-4; - if (lo >= -eps && hi <= 1 + eps) return UNIT; - if (lo >= -1 - eps && hi <= 1 + eps) return SIGNED_UNIT; - return UNBOUNDED; -} - -/** scipy.special.expit of a float32 as scipy computes it for float32 input, - * `1 / (1 + exp(-x))` with every step rounded to float32 (expf, then the - * sum, then the quotient). Each step is done in float64 and rounded once, - * which gives the float32 result for the sum and the quotient exactly and - * for the exponential except where glibc's expf is not correctly rounded. */ -export function expit32(x: number): number { - const e = Math.fround(Math.exp(-x)); - return Math.fround(1 / Math.fround(1 + e)); -} - -/** The prediction as a probability, [channels, z, y, x] float32, 0.5 at the - * model's decision boundary (view_labels.probability). - * - * The boundary depends on the output's activation: 0.5 on [0, 1] output (a - * sigmoid already applied), 0 on tanh's [-1, 1], and 0 on unbounded output, - * logits or a signed distance alike; each is turned into a probability. - * `channels` picks the channels (null: every one); `outputClass` is the - * served model's (ModelInfo.output_class), or read off the values of the - * picked channels when it has none. */ -export function probability(prediction: Block, outputClass: string | null | undefined, channels: number[] | null): Block { - const [C, ...spatial] = prediction.shape; - const n = prod(spatial); - if (prediction.data.length !== C * n) throw new Error(`a prediction of shape ${prediction.shape.join("×")} has ${prediction.data.length} values`); - const picked = channels ?? Array.from({ length: C }, (_, c) => c); - const missing = picked.find((c) => c >= C); - if (missing !== undefined) throw new Error(`The model has no channel ${missing}`); - if (!picked.length) throw new Error("The model has no channel to read"); - const out = new Float32Array(picked.length * n); - picked.forEach((c, k) => out.set(prediction.data.subarray(c * n, (c + 1) * n), k * n)); - let cls = outputClass || null; - if (!cls) { - let lo = Infinity, hi = -Infinity; - for (let i = 0; i < out.length; i++) { - if (out[i] < lo) lo = out[i]; - if (out[i] > hi) hi = out[i]; - } - cls = classifyOutputRange(lo, hi); - } - if (cls === UNIT) return { data: out, shape: [picked.length, ...spatial] }; - if (cls === SIGNED_UNIT) { - // (prediction + 1) / 2 in float32: the sum rounded, the halving exact. - for (let i = 0; i < out.length; i++) out[i] = Math.fround(out[i] + 1) / 2; - } else { - for (let i = 0; i < out.length; i++) out[i] = expit32(out[i]); - } - return { data: out, shape: [picked.length, ...spatial] }; -} - -/** The channels' mean, as numpy's float32 mean over axis 0 computes it: the - * channels added one after another in float32, then divided by their count - * in float32. */ -export function channelMean(block: Block): Float32Array { - const [C, ...spatial] = block.shape; - const n = prod(spatial); - const mean = block.data.slice(0, n); - for (let c = 1; c < C; c++) { - const plane = block.data.subarray(c * n, (c + 1) * n); - for (let i = 0; i < n; i++) mean[i] = Math.fround(mean[i] + plane[i]); - } - if (C > 1) for (let i = 0; i < n; i++) mean[i] = Math.fround(mean[i] / C); - return mean; -} - -/** Where the model says foreground (view_labels.foreground_mask): its - * probability, averaged over `channels`, over `threshold`, a probability (0.5 - * is the model's own boundary). numpy compares a float32 array with a - * Python float as float32, so the threshold is rounded to one first. 1 for - * foreground, over the block's z, y, x. */ -export function foregroundMask(prediction: Block, outputClass: string | null | undefined, channels: number[] | null, threshold = 0.5): Uint8Array { - const mean = channelMean(probability(prediction, outputClass, channels)); - const t = Math.fround(threshold); - const mask = new Uint8Array(mean.length); - for (let i = 0; i < mean.length; i++) mask[i] = mean[i] > t ? 1 : 0; - return mask; -} - -// --------------------------------------------------------------------------- -// The objects -// --------------------------------------------------------------------------- - -export interface SegmentOptions { - /** A probability, 0 to 1: the foreground boundary for components, the mutex watershed's bias. */ - threshold: number; - /** Objects of fewer voxels become background. */ - minSize: number; - /** 1, 2 or 3 (infer/label.ts asConnectivity): the components'. */ - connectivity: number; - /** Components found in each z slice on its own. */ - perSlice: boolean; -} - -/** The objects `method` makes of the prediction (view_labels.segment_prediction): - * 0 background, 1..n, over the block's z, y, x. - * - * "components": the foreground mask (foregroundMask over `channels`), an id - * per connected object (connectivity, perSlice, minSize). "mutex_watershed": - * the probability of the first offsets.length channels (the affinity - * channels: seedPlan puts them first, as the server's slice(0, len(offsets)) - * takes them), joined where the affinity is over the threshold - * (segment.mutex_watershed with the threshold as its bias; it ignores - * connectivity and per-slice). Any other method is refused; the route - * offers only methodsFor's. */ -export function segmentPrediction( - method: string, - prediction: Block, - outputClass: string | null | undefined, - channels: number[] | null, - offsets: number[][] | null, - options: SegmentOptions, -): Uint32Array { - const spatial = prediction.shape.slice(1); - if (spatial.length !== 3) throw new Error(`a seed segments (c, z, y, x) blocks, got shape ${prediction.shape.join("×")}`); - if (method === "components") { - const mask = foregroundMask(prediction, outputClass, channels, options.threshold); - return connectedComponents(mask, spatial, { connectivity: options.connectivity, minSize: options.minSize, perSlice: options.perSlice }); - } - if (method === "mutex_watershed") { - if (!offsets || !offsets.length) throw new Error("The model's affinity offsets are unknown, so it cannot seed by mutex_watershed"); - const affinityChannels = channels && channels.length === offsets.length ? channels : offsets.map((_, c) => c); - const affinities = probability(prediction, outputClass, affinityChannels); - const labels = mutexWatershed(affinities, offsets, options.threshold, options.minSize); - const out = new Uint32Array(labels.length); - for (let i = 0; i < labels.length; i++) out[i] = Number(labels[i]); - return out; - } - throw new Error(`method must be one of ${SEED_METHODS.join(", ")}, got ${JSON.stringify(method)}`); +/** A typed array's bytes to hand to the worker: its own buffer when it spans + * all of it (transferred, so the caller's copy is gone after), else a copy. */ +export function bufferOf(a: Float32Array): ArrayBuffer { + const whole = a.byteOffset === 0 && a.byteLength === a.buffer.byteLength && a.buffer instanceof ArrayBuffer; + return whole ? (a.buffer as ArrayBuffer) : (a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength) as ArrayBuffer); +} + +/** The objects of the mutex watershed (segment.mutex_watershed's, as the + * server's seed runs it): `affinities`, label_it.affinities' answer in [0, 1], + * joined where the affinity is over `threshold` (the bias), fragments + * below it background, objects of fewer than `minSize` voxels too; ids + * 1..n over the block's z, y, x. */ +export function mutexObjects(affinities: Block, offsets: number[][], threshold: number, minSize: number): Uint32Array { + const labels = mutexWatershed(affinities, offsets, threshold, minSize); + const out = new Uint32Array(labels.length); + for (let i = 0; i < labels.length; i++) out[i] = Number(labels[i]); + return out; +} + +/** A model's output block as a probability (label_it.probability over every + * channel), by the served model's output class (ModelInfo.output_class; + * null: read off the values): 0.5 at the model's decision boundary, + * whatever its activation. `block` is handed to the worker. */ +export async function probability(infer: Pick, block: Block, outputClass: string | null | undefined): Promise { + const bytes = await infer.python(`${LABEL_IT}:probability`, { shape: block.shape, output_class: outputClass ?? null }, [bufferOf(block.data)]); + return { data: new Float32Array(bytes), shape: block.shape }; } diff --git a/web/src/run-region/region.ts b/web/src/run-region/region.ts index 0eba7f9d..1b9dcc75 100644 --- a/web/src/run-region/region.ts +++ b/web/src/run-region/region.ts @@ -22,9 +22,8 @@ import { levelCornerNm, levelExtentNm } from "../ai-annotate/plane"; import { blobOf } from "../backend/serving"; import type { Chain, DatasetInfo, LayerSpec, ServedModel } from "../contracts"; -import { steady } from "../finetune/good-regions"; import { TYPED } from "../infer/dtype"; -import { geometryOf, type Level, outputShape } from "../infer/geometry"; +import { geometryOf, type Level, outputShape, steady } from "../infer/geometry"; import { outputChannels, outputDtype } from "../infer/post"; import { servedLevel } from "../label-it/prediction"; import { decodeArgs } from "../shared/args"; diff --git a/web/test/fixtures/ai_annotate.json b/web/test/fixtures/ai_annotate.json deleted file mode 100644 index c417e5c1..00000000 --- a/web/test/fixtures/ai_annotate.json +++ /dev/null @@ -1,2973 +0,0 @@ -{ - "_format": "Parity fixture for web/src/ai-annotate (geometry, plane, mask, labels, fill, prompts, organelles) against cellmap_flow/ai_annotate and finetune/session/fill.py; arrays are {dtype, shape, data} flat in C order; see make_ai_fixtures.py's docstring for the sections", - "seed": 20261015, - "constants": { - "max_image_side": 1024, - "plane_names": ["XY", "XZ", "YZ"] - }, - "screen": [ - { - "depth_axis": 0, - "display_dimensions": null, - "plane": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "to_screen": { - "dtype": "int32", - "shape": [2, 3], - "data": [2, 1, 0, 5, 4, 3] - }, - "screen_shape": [2, 3], - "image": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }, - "from_screen": { - "dtype": "int32", - "shape": [3, 4], - "data": [3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8] - } - }, - { - "depth_axis": 1, - "display_dimensions": null, - "plane": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "to_screen": { - "dtype": "int32", - "shape": [3, 2], - "data": [0, 3, 1, 4, 2, 5] - }, - "screen_shape": [3, 2], - "image": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }, - "from_screen": { - "dtype": "int32", - "shape": [4, 3], - "data": [0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11] - } - }, - { - "depth_axis": 2, - "display_dimensions": null, - "plane": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "to_screen": { - "dtype": "int32", - "shape": [3, 2], - "data": [0, 3, 1, 4, 2, 5] - }, - "screen_shape": [3, 2], - "image": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }, - "from_screen": { - "dtype": "int32", - "shape": [4, 3], - "data": [0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11] - } - }, - { - "depth_axis": 0, - "display_dimensions": ["x", "y", "z"], - "plane": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "to_screen": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "screen_shape": [2, 3], - "image": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }, - "from_screen": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - } - }, - { - "depth_axis": 1, - "display_dimensions": ["x", "y", "z"], - "plane": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "to_screen": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "screen_shape": [2, 3], - "image": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }, - "from_screen": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - } - }, - { - "depth_axis": 2, - "display_dimensions": ["x", "y", "z"], - "plane": { - "dtype": "int32", - "shape": [2, 3], - "data": [0, 1, 2, 3, 4, 5] - }, - "to_screen": { - "dtype": "int32", - "shape": [3, 2], - "data": [3, 0, 4, 1, 5, 2] - }, - "screen_shape": [3, 2], - "image": { - "dtype": "int32", - "shape": [3, 4], - "data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }, - "from_screen": { - "dtype": "int32", - "shape": [4, 3], - "data": [3, 7, 11, 2, 6, 10, 1, 5, 9, 0, 4, 8] - } - } - ], - "plan_plane": [ - { - "id": "centred_z", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [399.0, 544.0, 952.0], - "raw_shape_nm": [4.0, 512.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 68, 119], - "write_hi": [51, 132, 183], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [64, 64], - "plane_axes": [1, 2], - "write_shape": [64, 64], - "plane_name": "XY" - }, - { - "id": "centred_y", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 1, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 1, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [144.0, 801.0, 952.0], - "raw_shape_nm": [512.0, 4.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [18, 100, 119], - "write_hi": [82, 101, 183], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [63, 64], - "plane_axes": [0, 2], - "write_shape": [64, 64], - "plane_name": "XZ" - }, - { - "id": "centred_x", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 2, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 2, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [144.0, 544.0, 1204.0], - "raw_shape_nm": [512.0, 512.0, 4.0], - "image_shape": [128, 128], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [18, 68, 150], - "write_hi": [82, 132, 151], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [64, 64], - "plane_axes": [0, 1], - "write_shape": [64, 64], - "plane_name": "YZ" - }, - { - "id": "boundary", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [80.0, 800.0, 1200.0], - "depth_axis": 0, - "crop_size_px": 8, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [80.0, 800.0, 1200.0], - "raw_offset_nm": [78.0, 768.0, 1168.0], - "raw_shape_nm": [4.0, 64.0, 64.0], - "image_shape": [16, 16], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [10, 96, 146], - "write_hi": [11, 104, 154], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [8, 7], - "plane_axes": [1, 2], - "write_shape": [8, 8], - "plane_name": "XY" - }, - { - "id": "offset_100", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [100.0, 100.0, 100.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [103.0, 264.0, 344.0], - "depth_axis": 0, - "crop_size_px": 4, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [103.0, 264.0, 344.0], - "raw_offset_nm": [101.0, 248.0, 328.0], - "raw_shape_nm": [4.0, 32.0, 32.0], - "image_shape": [8, 8], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [0, 19, 29], - "write_hi": [1, 23, 33], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [4, 3], - "plane_axes": [1, 2], - "write_shape": [4, 4], - "plane_name": "XY" - }, - { - "id": "aniso_xz", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [808.0, 804.0, 1204.0], - "depth_axis": 1, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 1, - "point_nm": [808.0, 804.0, 1204.0], - "raw_offset_nm": [544.0, 802.0, 944.0], - "raw_shape_nm": [512.0, 4.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [8.0, 4.0, 4.0], - "write_lo": [34, 100, 118], - "write_hi": [66, 101, 182], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [65, 66], - "plane_axes": [0, 2], - "write_shape": [32, 64], - "plane_name": "XZ" - }, - { - "id": "aniso_xy", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [808.0, 804.0, 1204.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [808.0, 804.0, 1204.0], - "raw_offset_nm": [804.0, 544.0, 944.0], - "raw_shape_nm": [8.0, 512.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [8.0, 4.0, 4.0], - "write_lo": [50, 68, 118], - "write_hi": [51, 132, 182], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [65, 62], - "plane_axes": [1, 2], - "write_shape": [64, 64], - "plane_name": "XY" - }, - { - "id": "aniso_yz", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [808.0, 804.0, 1204.0], - "depth_axis": 2, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 2, - "point_nm": [808.0, 804.0, 1204.0], - "raw_offset_nm": [544.0, 544.0, 1202.0], - "raw_shape_nm": [512.0, 512.0, 4.0], - "image_shape": [128, 128], - "input_voxel_size": [8.0, 4.0, 4.0], - "write_lo": [34, 68, 150], - "write_hi": [66, 132, 151], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [65, 66], - "plane_axes": [0, 1], - "write_shape": [32, 64], - "plane_name": "YZ" - }, - { - "id": "io_differ_4nm", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 2000, 2000], - "point_nm": [400.0, 8000.0, 8000.0], - "depth_axis": 0, - "crop_size_px": 512, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [400.0, 8000.0, 8000.0], - "raw_offset_nm": [398.0, 5952.0, 5952.0], - "raw_shape_nm": [4.0, 4096.0, 4096.0], - "image_shape": [1024, 1024], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 744, 744], - "write_hi": [51, 1256, 1256], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [512, 511], - "plane_axes": [1, 2], - "write_shape": [512, 512], - "plane_name": "XY" - }, - { - "id": "io_same_8nm", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 2000, 2000], - "point_nm": [400.0, 8000.0, 8000.0], - "depth_axis": 0, - "crop_size_px": 512, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [400.0, 8000.0, 8000.0], - "raw_offset_nm": [396.0, 5952.0, 5952.0], - "raw_shape_nm": [8.0, 4096.0, 4096.0], - "image_shape": [512, 512], - "input_voxel_size": [8.0, 8.0, 8.0], - "write_lo": [50, 744, 744], - "write_hi": [51, 1256, 1256], - "resolution_nm": 8.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [256, 255], - "plane_axes": [1, 2], - "write_shape": [512, 512], - "plane_name": "XY" - }, - { - "id": "downsampled", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 2000, 2000], - "point_nm": [400.0, 8000.0, 8000.0], - "depth_axis": 0, - "crop_size_px": 1024, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [400.0, 8000.0, 8000.0], - "raw_offset_nm": [398.0, 3904.0, 3904.0], - "raw_shape_nm": [4.0, 8192.0, 8192.0], - "image_shape": [1024, 1024], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 488, 488], - "write_hi": [51, 1512, 1512], - "resolution_nm": 8.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [512, 511], - "plane_axes": [1, 2], - "write_shape": [1024, 1024], - "plane_name": "XY" - }, - { - "id": "downsampled_narrow", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 2000, 2000], - "point_nm": [400.0, 8000.0, 40.0], - "depth_axis": 0, - "crop_size_px": 1024, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [400.0, 8000.0, 40.0], - "raw_offset_nm": [398.0, 3904.0, 0.0], - "raw_shape_nm": [4.0, 8192.0, 4136.0], - "image_shape": [1024, 517], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 488, 0], - "write_hi": [51, 1512, 517], - "resolution_nm": 8.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [512, 511], - "plane_axes": [1, 2], - "write_shape": [1024, 517], - "plane_name": "XY" - }, - { - "id": "half_pixel_rounding", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 3000, 1001], - "point_nm": [400.0, 12000.0, 4000.0], - "depth_axis": 0, - "crop_size_px": 2048, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [400.0, 12000.0, 4000.0], - "raw_offset_nm": [398.0, 3808.0, 0.0], - "raw_shape_nm": [4.0, 16384.0, 8008.0], - "image_shape": [1024, 500], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 476, 0], - "write_hi": [51, 2524, 1001], - "resolution_nm": 16.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [512, 250], - "plane_axes": [1, 2], - "write_shape": [2048, 1001], - "plane_name": "XY" - }, - { - "id": "clipped_edge", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [404.0, 28.0, 2380.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [404.0, 28.0, 2380.0], - "raw_offset_nm": [402.0, 0.0, 2120.0], - "raw_shape_nm": [4.0, 280.0, 280.0], - "image_shape": [70, 70], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 0, 265], - "write_hi": [51, 35, 300], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [7, 4], - "plane_axes": [1, 2], - "write_shape": [35, 35], - "plane_name": "XY" - }, - { - "id": "data_box_clip", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": [[0.0, 0.0, 0.0], [800.0, 1600.0, 1210.0]], - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [399.0, 544.0, 952.0], - "raw_shape_nm": [4.0, 512.0, 256.0], - "image_shape": [128, 64], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 68, 119], - "write_hi": [51, 132, 151], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [64, 0], - "plane_axes": [1, 2], - "write_shape": [64, 32], - "plane_name": "XY" - }, - { - "id": "data_box_refused", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": [[0.0, 0.0, 0.0], [800.0, 1600.0, 1200.0]], - "display_dimensions": null, - "error": { - "category": "refused", - "message": "The point (401, 803, 1206) nm is outside the data (z, y, x 0..800, 0..1600, 0..1200 nm): move the view onto the data and try again.", - "http_status": 400 - } - }, - { - "id": "outside_before_z", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [-1.0, 800.0, 1200.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "error": { - "category": "refused", - "message": "The point (-1, 800, 1200) nm is outside the annotation volume (z, y, x 0..800, 0..1600, 0..2400 nm): move the view onto the data and try again.", - "http_status": 400 - } - }, - { - "id": "outside_past_x", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [400.0, 800.0, 2400.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "error": { - "category": "refused", - "message": "The point (400, 800, 2400) nm is outside the annotation volume (z, y, x 0..800, 0..1600, 0..2400 nm): move the view onto the data and try again.", - "http_status": 400 - } - }, - { - "id": "outside_fraction", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [400.0, -0.5, 1200.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "error": { - "category": "refused", - "message": "The point (400, -0.5, 1200) nm is outside the annotation volume (z, y, x 0..800, 0..1600, 0..2400 nm): move the view onto the data and try again.", - "http_status": 400 - } - }, - { - "id": "outside_at_end_z", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [804.0, 800.0, 1200.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": null, - "error": { - "category": "refused", - "message": "The point (804, 800, 1200) nm is outside the annotation volume (z, y, x 0..800, 0..1600, 0..2400 nm): move the view onto the data and try again.", - "http_status": 400 - } - }, - { - "id": "small_z", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 0, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [399.0, 736.0, 1144.0], - "raw_shape_nm": [4.0, 128.0, 128.0], - "image_shape": [32, 32], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 92, 143], - "write_hi": [51, 108, 159], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [16, 16], - "plane_axes": [1, 2], - "write_shape": [16, 16], - "plane_name": "XY" - }, - { - "id": "small_y", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 1, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 1, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [336.0, 801.0, 1144.0], - "raw_shape_nm": [128.0, 4.0, 128.0], - "image_shape": [32, 32], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [42, 100, 143], - "write_hi": [58, 101, 159], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [15, 16], - "plane_axes": [0, 2], - "write_shape": [16, 16], - "plane_name": "XZ" - }, - { - "id": "small_x", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 2, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 2, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [336.0, 736.0, 1204.0], - "raw_shape_nm": [128.0, 128.0, 4.0], - "image_shape": [32, 32], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [42, 92, 150], - "write_hi": [58, 108, 151], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [16, 16], - "plane_axes": [0, 1], - "write_shape": [16, 16], - "plane_name": "YZ" - }, - { - "id": "small_aniso_xz", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [808.0, 804.0, 1204.0], - "depth_axis": 1, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 1, - "point_nm": [808.0, 804.0, 1204.0], - "raw_offset_nm": [736.0, 802.0, 1136.0], - "raw_shape_nm": [128.0, 4.0, 128.0], - "image_shape": [32, 32], - "input_voxel_size": [8.0, 4.0, 4.0], - "write_lo": [46, 100, 142], - "write_hi": [54, 101, 158], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [17, 18], - "plane_axes": [0, 2], - "write_shape": [8, 16], - "plane_name": "XZ" - }, - { - "id": "small_aniso_yz", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [808.0, 804.0, 1204.0], - "depth_axis": 2, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 2, - "point_nm": [808.0, 804.0, 1204.0], - "raw_offset_nm": [736.0, 736.0, 1202.0], - "raw_shape_nm": [128.0, 128.0, 4.0], - "image_shape": [32, 32], - "input_voxel_size": [8.0, 4.0, 4.0], - "write_lo": [46, 92, 150], - "write_hi": [54, 108, 151], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [17, 18], - "plane_axes": [0, 1], - "write_shape": [8, 16], - "plane_name": "YZ" - }, - { - "id": "small_clipped", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [404.0, 28.0, 2380.0], - "depth_axis": 0, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [404.0, 28.0, 2380.0], - "raw_offset_nm": [402.0, 0.0, 2312.0], - "raw_shape_nm": [4.0, 88.0, 88.0], - "image_shape": [22, 22], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 0, 289], - "write_hi": [51, 11, 300], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [7, 4], - "plane_axes": [1, 2], - "write_shape": [11, 11], - "plane_name": "XY" - }, - { - "id": "small_same", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 0, - "crop_size_px": 16, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 0, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [397.0, 736.0, 1144.0], - "raw_shape_nm": [8.0, 128.0, 128.0], - "image_shape": [16, 16], - "input_voxel_size": [8.0, 8.0, 8.0], - "write_lo": [50, 92, 143], - "write_hi": [51, 108, 159], - "resolution_nm": 8.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [8, 8], - "plane_axes": [1, 2], - "write_shape": [16, 16], - "plane_name": "XY" - }, - { - "id": "small_offset_100", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [100.0, 100.0, 100.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [103.0, 264.0, 344.0], - "depth_axis": 1, - "crop_size_px": 12, - "data_box_nm": null, - "display_dimensions": null, - "plan": { - "depth_axis": 1, - "point_nm": [103.0, 264.0, 344.0], - "raw_offset_nm": [96.0, 262.0, 296.0], - "raw_shape_nm": [56.0, 4.0, 96.0], - "image_shape": [24, 14], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [0, 21, 25], - "write_hi": [7, 22, 37], - "resolution_nm": 4.0, - "display_dimensions": ["z", "y", "x"] - }, - "click_px": [12, 1], - "plane_axes": [0, 2], - "write_shape": [7, 12], - "plane_name": "XZ" - }, - { - "id": "centred_z_shown_xyz", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 0, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": ["x", "y", "z"], - "plan": { - "depth_axis": 0, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [399.0, 544.0, 952.0], - "raw_shape_nm": [4.0, 512.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [50, 68, 119], - "write_hi": [51, 132, 183], - "resolution_nm": 4.0, - "display_dimensions": ["x", "y", "z"] - }, - "click_px": [64, 63], - "plane_axes": [1, 2], - "write_shape": [64, 64], - "plane_name": "XY" - }, - { - "id": "centred_y_shown_xyz", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 1, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": ["x", "y", "z"], - "plan": { - "depth_axis": 1, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [144.0, 801.0, 952.0], - "raw_shape_nm": [512.0, 4.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [18, 100, 119], - "write_hi": [82, 101, 183], - "resolution_nm": 4.0, - "display_dimensions": ["x", "y", "z"] - }, - "click_px": [64, 63], - "plane_axes": [0, 2], - "write_shape": [64, 64], - "plane_name": "XZ" - }, - { - "id": "centred_x_shown_xyz", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [401.0, 803.0, 1206.0], - "depth_axis": 2, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": ["x", "y", "z"], - "plan": { - "depth_axis": 2, - "point_nm": [401.0, 803.0, 1206.0], - "raw_offset_nm": [144.0, 544.0, 1204.0], - "raw_shape_nm": [512.0, 512.0, 4.0], - "image_shape": [128, 128], - "input_voxel_size": [4.0, 4.0, 4.0], - "write_lo": [18, 68, 150], - "write_hi": [82, 132, 151], - "resolution_nm": 4.0, - "display_dimensions": ["x", "y", "z"] - }, - "click_px": [64, 63], - "plane_axes": [0, 1], - "write_shape": [64, 64], - "plane_name": "YZ" - }, - { - "id": "aniso_xz_shown_xyz", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "volume_shape": [100, 200, 300], - "point_nm": [808.0, 804.0, 1204.0], - "depth_axis": 1, - "crop_size_px": 64, - "data_box_nm": null, - "display_dimensions": ["x", "y", "z"], - "plan": { - "depth_axis": 1, - "point_nm": [808.0, 804.0, 1204.0], - "raw_offset_nm": [544.0, 802.0, 944.0], - "raw_shape_nm": [512.0, 4.0, 512.0], - "image_shape": [128, 128], - "input_voxel_size": [8.0, 4.0, 4.0], - "write_lo": [34, 100, 118], - "write_hi": [66, 101, 182], - "resolution_nm": 4.0, - "display_dimensions": ["x", "y", "z"] - }, - "click_px": [66, 65], - "plane_axes": [0, 2], - "write_shape": [32, 64], - "plane_name": "XZ" - } - ], - "raw_box": [ - { - "plan": "centred_z", - "voxel_size": [4, 4, 4], - "corner": [0, 0, 0], - "begin": [100, 136, 238], - "count": [1, 128, 128] - }, - { - "plan": "centred_z", - "voxel_size": [4, 4, 4], - "corner": [-1, -1, -1], - "begin": [100, 136, 238], - "count": [1, 128, 128] - }, - { - "plan": "centred_z", - "voxel_size": [4, 4, 4], - "corner": [1, 1, 1], - "begin": [100, 136, 238], - "count": [1, 128, 128] - }, - { - "plan": "centred_z", - "voxel_size": [4, 4, 4], - "corner": [3, 3, 3], - "begin": [99, 135, 237], - "count": [1, 128, 128] - }, - { - "plan": "centred_z", - "voxel_size": [8, 8, 8], - "corner": [0, 0, 0], - "begin": [50, 68, 119], - "count": [1, 64, 64] - }, - { - "plan": "centred_y", - "voxel_size": [4, 4, 4], - "corner": [0, 0, 0], - "begin": [36, 200, 238], - "count": [128, 1, 128] - }, - { - "plan": "aniso_xz", - "voxel_size": [8, 4, 4], - "corner": [0, 0, 0], - "begin": [68, 201, 236], - "count": [64, 1, 128] - }, - { - "plan": "aniso_xz", - "voxel_size": [8, 4, 4], - "corner": [-4, -2, -2], - "begin": [69, 201, 237], - "count": [64, 1, 128] - }, - { - "plan": "downsampled", - "voxel_size": [4, 4, 4], - "corner": [0, 0, 0], - "begin": [100, 976, 976], - "count": [1, 2048, 2048] - }, - { - "plan": "small_offset_100", - "voxel_size": [4, 4, 4], - "corner": [96, 96, 96], - "begin": [0, 42, 50], - "count": [14, 1, 24] - } - ], - "extract_mask": { - "image": { - "dtype": "uint8", - "shape": [3, 6, 3], - "data": [255, 0, 0, 230, 20, 10, 200, 0, 0, 255, 128, 0, 255, 100, 0, 128, 0, 255, 120, 10, 250, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 255, 0, 0, 255, 128, 0, 0, 255, 0, 255, 255, 255, 0, 255, 255, 0, 128, 60, 60, 60, 254, 1, 1] - }, - "cases": [ - { - "color_name": "bright blue", - "target_rgb": [0, 0, 255], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright blue", - "target_rgb": [0, 0, 255], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright cyan", - "target_rgb": [0, 255, 255], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0] - } - }, - { - "color_name": "bright cyan", - "target_rgb": [0, 255, 255], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0] - } - }, - { - "color_name": "bright green", - "target_rgb": [0, 255, 0], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright green", - "target_rgb": [0, 255, 0], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright magenta", - "target_rgb": [255, 0, 255], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0] - } - }, - { - "color_name": "bright magenta", - "target_rgb": [255, 0, 255], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0] - } - }, - { - "color_name": "bright orange", - "target_rgb": [255, 128, 0], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright orange", - "target_rgb": [255, 128, 0], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1] - } - }, - { - "color_name": "bright purple", - "target_rgb": [128, 0, 255], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright purple", - "target_rgb": [128, 0, 255], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0] - } - }, - { - "color_name": "bright red", - "target_rgb": [255, 0, 0], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] - } - }, - { - "color_name": "bright red", - "target_rgb": [255, 0, 0], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1] - } - }, - { - "color_name": "bright rose", - "target_rgb": [255, 0, 128], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] - } - }, - { - "color_name": "bright rose", - "target_rgb": [255, 0, 128], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1] - } - }, - { - "color_name": "bright spring green", - "target_rgb": [0, 255, 128], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright spring green", - "target_rgb": [0, 255, 128], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0] - } - }, - { - "color_name": "bright yellow", - "target_rgb": [255, 255, 0], - "threshold": 200.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] - } - }, - { - "color_name": "bright yellow", - "target_rgb": [255, 255, 0], - "threshold": 100.0, - "mask": { - "dtype": "uint8", - "shape": [3, 6], - "data": [0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - ] - }, - "normalize_to_uint8": [ - { - "id": "float32", - "plane": { - "dtype": "float32", - "shape": [6, 7], - "data": [50000.0, -1152.2083740234375, -1705.8636474609375, -590.4991455078125, -40.236202239990234, 228.69264221191406, 173.63519287109375, 187.94003295898438, 537.189697265625, 1089.5970458984375, 504.8616943359375, 1757.4986572265625, -183.7647705078125, -1496.898193359375, -2200.866943359375, 66.5318832397461, -717.7806396484375, -298.508544921875, 162.2704620361328, 331.0013427734375, -1410.740478515625, 787.4243774414062, 557.8081665039062, -413.27294921875, -556.0765380859375, -181.49073791503906, -492.390869140625, -32.62004852294922, -1172.789306640625, -1647.2293701171875, 830.494384765625, 705.5691528320312, 238.1824951171875, 212.0449676513672, -95.24925231933594, 224.02288818359375, 745.9205932617188, 696.5292358398438, -799.8114013671875, 68.2872314453125, 2068.43994140625, -50000.0] - }, - "uint8": { - "dtype": "uint8", - "shape": [6, 7], - "data": [255, 122, 120, 125, 127, 128, 128, 128, 129, 132, 129, 134, 126, 121, 118, 127, 124, 126, 128, 129, 121, 130, 129, 125, 125, 126, 125, 127, 122, 120, 131, 130, 128, 128, 127, 128, 130, 130, 124, 127, 136, 0] - } - }, - { - "id": "uint16", - "plane": { - "dtype": "uint16", - "shape": [5, 8], - "data": [32975, 17711, 32004, 10026, 36425, 8198, 2351, 13637, 4434, 16965, 35151, 35329, 35646, 35365, 31463, 15062, 35732, 27632, 31879, 17699, 5251, 10496, 29304, 14460, 31386, 19424, 1986, 7203, 1534, 27167, 8386, 22798, 6035, 2860, 15103, 21720, 3908, 37493, 12865, 7356] - }, - "uint8": { - "dtype": "uint8", - "shape": [5, 8], - "data": [225, 115, 218, 59, 250, 46, 4, 85, 19, 109, 241, 242, 244, 242, 214, 96, 245, 186, 217, 115, 25, 63, 198, 91, 213, 127, 1, 39, 0, 183, 48, 152, 31, 8, 96, 144, 15, 255, 80, 40] - } - }, - { - "id": "uint8", - "plane": { - "dtype": "uint8", - "shape": [4, 5], - "data": [249, 27, 77, 140, 228, 176, 54, 146, 176, 31, 212, 93, 29, 35, 196, 195, 43, 192, 11, 212] - }, - "uint8": { - "dtype": "uint8", - "shape": [4, 5], - "data": [249, 27, 77, 140, 228, 176, 54, 146, 176, 31, 212, 93, 29, 35, 196, 195, 43, 192, 11, 212] - } - } - ], - "resize": [ - { - "id": "shrink_whole", - "plane": { - "dtype": "float32", - "shape": [6, 8], - "data": [4.8418073654174805, 58.598716735839844, 247.53115844726562, 208.18173217773438, 70.31044006347656, 208.61183166503906, 237.04306030273438, 105.00074768066406, 63.71949005126953, 177.95140075683594, 253.3589630126953, 47.067161560058594, 249.2488555908203, 103.6622543334961, 49.654048919677734, 229.23858642578125, 82.7291030883789, 135.62962341308594, 34.56446075439453, 133.77853393554688, 161.12364196777344, 48.250465393066406, 115.10352325439453, 19.145578384399414, 103.4647445678711, 9.100686073303223, 171.9349822998047, 105.1932601928711, 215.2298126220703, 49.707313537597656, 209.74526977539062, 167.82582092285156, 69.36170196533203, 75.00361633300781, 56.98625183105469, 172.51795959472656, 173.35791015625, 11.396627426147461, 41.433631896972656, 1.3676635026931763, 162.30081176757812, 175.2137908935547, 196.14852905273438, 123.80371856689453, 66.34668731689453, 215.43597412109375, 173.52957153320312, 78.08450317382812] - }, - "shape": [3, 4], - "resized": { - "dtype": "float32", - "shape": [3, 4], - "data": [76.2778549194336, 189.03475952148438, 157.95834350585938, 155.23411560058594, 82.73104095458984, 111.36780548095703, 118.57781219482422, 127.95504760742188, 120.46997833251953, 137.36410522460938, 116.63429260253906, 73.60384368896484] - } - }, - { - "id": "shrink_fraction", - "plane": { - "dtype": "float32", - "shape": [7, 5], - "data": [176.5048370361328, 249.45452880859375, 188.679443359375, 150.9835662841797, 157.37672424316406, 36.327762603759766, 34.579044342041016, 17.498384475708008, 24.35969352722168, 122.40406036376953, 176.5192108154297, 83.41759490966797, 48.878273010253906, 206.18028259277344, 6.430037021636963, 44.88189697265625, 58.619041442871094, 233.7783203125, 175.8289031982422, 139.73129272460938, 201.36093139648438, 78.46924591064453, 210.13308715820312, 36.36777114868164, 24.198589324951172, 82.72998809814453, 29.173744201660156, 189.07911682128906, 112.01316833496094, 111.20489501953125, 200.74295043945312, 138.9113311767578, 156.77264404296875, 44.54049301147461, 18.559223175048828] - }, - "shape": [3, 2], - "resized": { - "dtype": "float32", - "shape": [3, 2], - "data": [117.17400360107422, 113.781005859375, 126.22862243652344, 98.12281036376953, 132.90162658691406, 71.5794448852539] - } - }, - { - "id": "grow_whole", - "plane": { - "dtype": "float32", - "shape": [3, 4], - "data": [244.4473114013672, 216.5948486328125, 127.6141357421875, 149.8857879638672, 102.91183471679688, 183.01937866210938, 39.171546936035156, 62.58511734008789, 229.920166015625, 33.944244384765625, 77.0363540649414, 4.6431565284729] - }, - "shape": [6, 8], - "resized": { - "dtype": "float32", - "shape": [6, 8], - "data": [244.4473114013672, 237.48419189453125, 223.55796813964844, 194.34967041015625, 149.85931396484375, 133.1820526123047, 144.31787109375, 149.8857879638672, 209.06344604492188, 208.8478240966797, 208.41659545898438, 182.526611328125, 131.1778564453125, 111.14277648925781, 122.42133331298828, 128.0606231689453, 138.2957000732422, 151.57508850097656, 178.1338653564453, 158.88047790527344, 93.81495666503906, 67.06421661376953, 78.62825775146484, 84.41028594970703, 134.66390991210938, 137.43557739257812, 142.9789276123047, 121.47238159179688, 72.91596221923828, 48.50321960449219, 48.23415756225586, 48.09962844848633, 198.1680908203125, 166.4293212890625, 102.95178985595703, 70.30230712890625, 68.48087310791016, 55.459774017333984, 31.23902130126953, 19.128646850585938, 229.920166015625, 180.92617797851562, 82.93822479248047, 44.71726989746094, 66.2633285522461, 58.938053131103516, 22.741455078125, 4.6431565284729] - } - }, - { - "id": "grow_fraction", - "plane": { - "dtype": "float32", - "shape": [3, 3], - "data": [224.85916137695312, 221.61819458007812, 31.683238983154297, 5.012679576873779, 199.85366821289062, 20.00929069519043, 246.051513671875, 154.6479034423828, 106.34392547607422] - }, - "shape": [7, 5], - "resized": { - "dtype": "float32", - "shape": [7, 5], - "data": [224.85916137695312, 223.56277465820312, 221.61819458007812, 107.65721893310547, 31.683238983154297, 193.4525146484375, 203.47509765625, 218.50897216796875, 105.41291046142578, 30.015531539916992, 99.23259735107422, 143.2120819091797, 209.1813201904297, 98.67997741699219, 25.01241111755371, 5.012679576873779, 82.9490737915039, 199.85366821289062, 91.9470443725586, 20.00929069519043, 108.3150405883789, 137.18092346191406, 180.47976684570312, 106.39781951904297, 57.009849548339844, 211.61740112304688, 191.41278076171875, 161.10586547851562, 120.84859466552734, 94.01040649414062, 246.051513671875, 209.4900665283203, 154.6479034423828, 125.66551971435547, 106.34392547607422] - } - }, - { - "id": "mixed", - "plane": { - "dtype": "float32", - "shape": [6, 3], - "data": [89.0926284790039, 77.49437713623047, 22.00775146484375, 206.72366333007812, 87.15647888183594, 3.218829870223999, 92.68299102783203, 243.3738250732422, 157.32821655273438, 21.61867332458496, 153.82293701171875, 4.384954929351807, 150.63897705078125, 220.2730712890625, 30.844234466552734, 89.37202453613281, 132.76185607910156, 43.41782760620117] - }, - "shape": [3, 6], - "resized": { - "dtype": "float32", - "shape": [3, 6], - "data": [140.0188446044922, 131.3472137451172, 114.00396728515625, 87.32096862792969, 51.29822540283203, 33.28684997558594, 87.53345489501953, 112.49446105957031, 162.41647338867188, 156.75819396972656, 95.51960754394531, 64.90032196044922, 105.95024108886719, 122.78152465820312, 156.44410705566406, 138.06979370117188, 67.65861511230469, 32.45302200317383] - } - }, - { - "id": "same", - "plane": { - "dtype": "float32", - "shape": [5, 5], - "data": [10.449776649475098, 160.76181030273438, 75.88026428222656, 139.53994750976562, 28.477813720703125, 230.5832977294922, 19.77800750732422, 171.4012451171875, 250.4491424560547, 137.15228271484375, 117.83248901367188, 187.5431671142578, 6.102634429931641, 157.40097045898438, 155.74728393554688, 66.65658569335938, 164.2717742919922, 134.42832946777344, 204.00572204589844, 211.5004425048828, 3.6458122730255127, 214.66033935546875, 168.81105041503906, 71.76902770996094, 78.41962432861328] - }, - "shape": [5, 5], - "resized": { - "dtype": "float32", - "shape": [5, 5], - "data": [10.449776649475098, 160.76181030273438, 75.88026428222656, 139.53994750976562, 28.477813720703125, 230.5832977294922, 19.77800750732422, 171.4012451171875, 250.4491424560547, 137.15228271484375, 117.83248901367188, 187.5431671142578, 6.102634429931641, 157.40097045898438, 155.74728393554688, 66.65658569335938, 164.2717742919922, 134.42832946777344, 204.00572204589844, 211.5004425048828, 3.6458122730255127, 214.66033935546875, 168.81105041503906, 71.76902770996094, 78.41962432861328] - } - }, - { - "id": "rows_only", - "plane": { - "dtype": "float32", - "shape": [8, 8], - "data": [133.2549285888672, 104.86796569824219, 66.81982421875, 136.66639709472656, 178.95010375976562, 135.51577758789062, 93.1607437133789, 160.3506317138672, 84.96350860595703, 233.3159942626953, 215.80519104003906, 149.8032989501953, 44.73310470581055, 63.903106689453125, 121.79487609863281, 54.348575592041016, 247.9498291015625, 196.6631317138672, 231.48983764648438, 95.7767562866211, 145.0947265625, 139.33065795898438, 204.54612731933594, 106.20556640625, 90.85279083251953, 170.95225524902344, 194.64630126953125, 99.73599243164062, 228.9347686767578, 129.08265686035156, 111.78458404541016, 183.25582885742188, 126.46332550048828, 78.14802551269531, 224.34413146972656, 153.4944610595703, 207.29071044921875, 0.6476727724075317, 240.50831604003906, 220.83938598632812, 112.13317108154297, 194.0515899658203, 91.76722717285156, 41.176048278808594, 175.20068359375, 3.8353214263916016, 92.54925537109375, 184.6748809814453, 192.07894897460938, 12.237632751464844, 196.83285522460938, 4.926394462585449, 229.55809020996094, 165.96192932128906, 79.39179229736328, 83.66461181640625, 87.99809265136719, 25.777103424072266, 112.2139663696289, 60.253971099853516, 213.13580322265625, 127.19013977050781, 252.26075744628906, 52.91928482055664] - }, - "shape": [5, 8], - "resized": { - "dtype": "float32", - "shape": [5, 8], - "data": [109.10922241210938, 169.09197998046875, 141.3125, 143.23484802246094, 111.84160614013672, 99.70944213867188, 107.47781372070312, 107.34960174560547, 247.9498291015625, 196.6631317138672, 231.48983764648438, 95.7767562866211, 145.0947265625, 139.33065795898438, 204.54612731933594, 106.20556640625, 108.6580581665039, 124.55014038085938, 209.49520874023438, 126.61522674560547, 218.11273193359375, 64.86516571044922, 176.14645385742188, 202.047607421875, 112.13317108154297, 194.0515899658203, 91.76722717285156, 41.176048278808594, 175.20068359375, 3.8353214263916016, 92.54925537109375, 184.6748809814453, 140.03851318359375, 19.007368087768555, 154.52340698242188, 32.59018325805664, 221.34695434570312, 146.57603454589844, 165.82627868652344, 68.29194641113281] - } - }, - { - "id": "cols_only", - "plane": { - "dtype": "float32", - "shape": [4, 9], - "data": [37.59719467163086, 134.4566650390625, 231.8061981201172, 242.94570922851562, 177.35028076171875, 11.533034324645996, 133.490966796875, 173.5601348876953, 25.469079971313477, 98.46062469482422, 237.95352172851562, 93.04361724853516, 13.235435485839844, 201.2071533203125, 240.74813842773438, 75.55078125, 84.68958282470703, 228.80038452148438, 191.5204620361328, 78.66715240478516, 45.19007873535156, 111.18505859375, 179.61424255371094, 250.43170166015625, 80.59233093261719, 59.887123107910156, 38.563575744628906, 208.8997039794922, 144.24261474609375, 184.17506408691406, 193.3263702392578, 131.11520385742188, 111.61336517333984, 214.70468139648438, 224.66859436035156, 218.3437957763672] - }, - "shape": [4, 3], - "resized": { - "dtype": "float32", - "shape": [4, 3], - "data": [134.62002563476562, 143.94300842285156, 110.84005737304688, 143.152587890625, 151.73023986816406, 129.6802520751953, 105.12590026855469, 180.41033935546875, 59.68101119995117, 179.1057891845703, 145.35165405273438, 219.23902893066406] - } - }, - { - "id": "one_row", - "plane": { - "dtype": "float32", - "shape": [1, 6], - "data": [76.7770004272461, 226.05409240722656, 80.19491577148438, 65.04953002929688, 38.434722900390625, 233.8048858642578] - }, - "shape": [1, 4], - "resized": { - "dtype": "float32", - "shape": [1, 4], - "data": [151.41554260253906, 80.19491577148438, 51.74212646484375, 233.8048858642578] - } - } - ], - "mask_to_write_shape": [ - { - "id": "small_z", - "plan": "small_z", - "mask": { - "dtype": "uint8", - "shape": [32, 32], - "data": [0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0] - }, - "write": { - "dtype": "int32", - "shape": [16, 16], - "data": [0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 0, 4, 0, 5, 1, 1, 1, 1, 1, 0, 0, 0, 6, 3, 0, 0, 0, 4, 4, 0, 1, 0, 0, 0, 1, 0, 0, 7, 0, 8, 9, 10, 11, 12, 12, 4, 1, 1, 8, 8, 13, 0, 0, 0, 8, 8, 0, 14, 15, 0, 0, 0, 1, 1, 0, 8, 8, 8, 8, 8, 8, 8, 0, 0, 16, 16, 0, 0, 0, 17, 0, 18, 8, 8, 0, 8, 8, 0, 0, 0, 16, 0, 19, 1, 0, 17, 0, 0, 0, 20, 20, 8, 21, 0, 22, 22, 0, 0, 0, 0, 17, 0, 17, 23, 0, 0, 21, 0, 21, 21, 22, 0, 24, 0, 0, 0, 0, 25, 0, 26, 26, 26, 0, 21, 21, 21, 0, 27, 0, 0, 28, 29, 29, 0, 0, 0, 30, 0, 31, 0, 0, 32, 0, 0, 0, 28, 28, 0, 0, 0, 33, 0, 30, 0, 0, 0, 21, 0, 0, 34, 0, 28, 0, 33, 33, 33, 0, 0, 0, 0, 0, 0, 0, 35, 35, 34, 36, 36, 37, 38, 33, 0, 33, 0, 35, 35, 0, 35, 0, 35, 0, 34, 36, 36, 0, 0, 39, 0, 0, 35, 0, 0, 35, 0, 0, 0, 0, 34, 40, 0, 0, 0, 0, 0, 35, 35, 41, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 35, 35, 35, 35, 0, 43, 0, 0, 44] - } - }, - { - "id": "small_y", - "plan": "small_y", - "mask": { - "dtype": "uint8", - "shape": [32, 32], - "data": [0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1] - }, - "write": { - "dtype": "int32", - "shape": [16, 16], - "data": [0, 0, 1, 0, 0, 0, 0, 2, 0, 3, 3, 0, 3, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 2, 2, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 4, 4, 2, 2, 0, 2, 2, 5, 3, 0, 3, 0, 0, 0, 6, 0, 7, 0, 0, 0, 8, 0, 8, 8, 9, 0, 0, 0, 10, 6, 6, 6, 0, 8, 8, 0, 0, 8, 8, 11, 0, 0, 12, 12, 0, 6, 6, 0, 6, 0, 0, 8, 0, 8, 0, 0, 13, 13, 0, 12, 14, 6, 0, 0, 0, 6, 6, 0, 8, 8, 0, 0, 0, 15, 12, 0, 0, 0, 6, 6, 6, 6, 0, 0, 0, 0, 8, 0, 16, 0, 0, 0, 0, 17, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 16, 18, 18, 19, 0, 0, 17, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 20, 20, 0, 21, 17, 17, 0, 22, 0, 0, 8, 8, 0, 0, 8, 8, 0, 20, 0, 21, 0, 22, 22, 0, 23, 0, 8, 8, 0, 0, 8, 0, 20, 0, 0, 21, 24, 0, 22, 25, 0, 8, 8, 8, 0, 8, 0, 8, 8, 8, 0, 21, 22, 22, 0, 22, 22, 26, 0, 0, 0, 8, 27, 28, 0, 29, 30, 0, 0, 22, 22, 22, 22, 0, 26, 31, 0, 0, 32, 0, 33, 0, 0, 0, 0, 34, 0, 0, 22, 0, 0, 31, 0, 0, 32, 35, 0, 36, 0, 37] - } - }, - { - "id": "small_x", - "plan": "small_x", - "mask": { - "dtype": "uint8", - "shape": [32, 32], - "data": [0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1] - }, - "write": { - "dtype": "int32", - "shape": [16, 16], - "data": [1, 1, 0, 2, 2, 0, 3, 3, 3, 3, 0, 4, 4, 0, 0, 0, 0, 1, 0, 5, 0, 3, 3, 3, 3, 0, 6, 7, 8, 0, 9, 0, 0, 0, 0, 5, 0, 3, 3, 0, 10, 11, 12, 0, 0, 0, 0, 0, 0, 0, 0, 5, 13, 3, 3, 3, 3, 3, 0, 0, 0, 0, 14, 15, 16, 16, 0, 0, 0, 3, 0, 3, 0, 3, 3, 0, 14, 0, 14, 14, 16, 0, 3, 0, 0, 3, 0, 3, 0, 3, 3, 3, 0, 14, 14, 0, 0, 0, 3, 17, 0, 3, 3, 3, 3, 3, 0, 3, 3, 0, 3, 3, 0, 0, 0, 17, 0, 0, 0, 18, 18, 3, 0, 0, 0, 0, 3, 3, 3, 3, 0, 3, 3, 0, 0, 0, 19, 0, 0, 0, 0, 0, 3, 20, 3, 3, 3, 3, 0, 3, 0, 0, 19, 3, 0, 0, 3, 0, 0, 0, 3, 3, 0, 0, 3, 3, 0, 21, 22, 3, 3, 0, 0, 0, 0, 0, 23, 3, 3, 3, 3, 0, 3, 0, 0, 3, 3, 0, 0, 3, 24, 24, 3, 3, 25, 3, 0, 26, 3, 0, 3, 3, 0, 0, 3, 0, 0, 24, 0, 0, 0, 3, 0, 26, 27, 3, 3, 3, 0, 0, 0, 0, 28, 0, 0, 0, 29, 26, 26, 26, 0, 30, 0, 0, 3, 0, 31, 0, 0, 32, 0, 33, 34, 0, 30, 30, 30, 30, 35, 3, 0, 36, 0, 0, 32, 32] - } - }, - { - "id": "small_aniso_xz", - "plan": "small_aniso_xz", - "mask": { - "dtype": "uint8", - "shape": [32, 32], - "data": [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1] - }, - "write": { - "dtype": "int32", - "shape": [8, 16], - "data": [1, 0, 0, 2, 0, 1, 1, 1, 0, 3, 0, 0, 0, 0, 4, 0, 5, 0, 0, 0, 0, 1, 1, 0, 0, 0, 3, 0, 6, 0, 7, 8, 9, 0, 0, 0, 1, 1, 0, 10, 0, 0, 11, 0, 0, 12, 7, 0, 0, 9, 13, 13, 0, 1, 0, 0, 0, 10, 14, 15, 15, 0, 0, 0, 9, 9, 0, 0, 0, 0, 16, 17, 0, 0, 0, 0, 15, 15, 0, 0, 18, 18, 0, 19, 0, 0, 0, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 0, 20, 19, 0, 21, 0, 0, 22, 23, 16, 16, 0, 0, 0, 24, 25, 0, 20, 0, 0, 21, 21, 26, 27, 27, 27, 16, 16, 0, 0, 0] - } - }, - { - "id": "small_aniso_yz", - "plan": "small_aniso_yz", - "mask": { - "dtype": "uint8", - "shape": [32, 32], - "data": [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1] - }, - "write": { - "dtype": "int32", - "shape": [8, 16], - "data": [0, 0, 1, 0, 2, 2, 0, 0, 3, 3, 0, 4, 4, 5, 0, 0, 0, 0, 6, 6, 7, 8, 9, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 6, 0, 10, 0, 0, 0, 0, 3, 0, 3, 3, 0, 3, 0, 0, 0, 0, 11, 11, 0, 0, 0, 12, 0, 0, 3, 3, 0, 13, 13, 13, 0, 0, 14, 14, 0, 0, 12, 12, 12, 0, 15, 16, 0, 0, 16, 0, 17, 17, 14, 0, 0, 18, 0, 0, 0, 0, 0, 0, 16, 0, 16, 19, 17, 0, 20, 0, 21, 21, 21, 21, 21, 22, 23, 0, 16, 16, 24, 0, 0, 25, 26, 21, 0, 0, 0, 27, 0, 0, 0, 0, 0, 28, 0, 0] - } - }, - { - "id": "small_clipped", - "plan": "small_clipped", - "mask": { - "dtype": "uint8", - "shape": [22, 22], - "data": [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1] - }, - "write": { - "dtype": "int32", - "shape": [11, 11], - "data": [0, 1, 2, 3, 3, 0, 0, 4, 4, 0, 0, 0, 1, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 6, 0, 6, 6, 0, 0, 0, 7, 0, 0, 8, 8, 6, 6, 0, 0, 9, 9, 0, 0, 0, 8, 8, 6, 6, 6, 0, 10, 0, 0, 7, 0, 11, 11, 6, 6, 12, 12, 12, 13, 0, 0, 14, 15, 0, 0, 6, 0, 12, 12, 12, 0, 14, 14, 15, 16, 0, 0, 0, 0, 17, 12, 12, 12, 0, 0, 15, 0, 0, 0, 0, 18, 0, 0, 12, 0, 15, 15, 0, 0, 0, 19, 0, 12, 12, 12, 0, 15, 0, 20, 21, 22, 19, 0, 0, 0, 0, 12] - } - }, - { - "id": "small_same", - "plan": "small_same", - "mask": { - "dtype": "uint8", - "shape": [16, 16], - "data": [0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0] - }, - "write": { - "dtype": "int32", - "shape": [16, 16], - "data": [1, 0, 2, 0, 0, 3, 3, 3, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 3, 0, 3, 0, 3, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 4, 0, 0, 0, 5, 6, 6, 6, 6, 0, 0, 0, 3, 3, 0, 0, 0, 0, 7, 0, 5, 6, 6, 6, 0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 9, 9, 0, 7, 7, 7, 7, 0, 0, 10, 0, 8, 8, 0, 0, 0, 9, 0, 11, 0, 0, 7, 0, 12, 0, 10, 0, 0, 0, 13, 0, 14, 0, 0, 11, 0, 0, 7, 0, 12, 15, 0, 16, 16, 16, 0, 14, 14, 0, 0, 11, 11, 11, 0, 0, 12, 0, 16, 16, 0, 16, 16, 0, 14, 0, 0, 11, 0, 0, 0, 12, 12, 0, 16, 16, 16, 0, 16, 0, 0, 11, 11, 11, 11, 11, 11, 0, 0, 0, 0, 0, 16, 0, 16, 16, 16, 0, 0, 11, 11, 11, 0, 0, 17, 16, 16, 16, 16, 0, 0, 0, 0, 18, 0, 11, 0, 0, 0, 19, 0, 16, 16, 0, 0, 0, 18, 0, 18, 18, 0, 0, 20, 0, 21, 0, 21, 16, 0, 0, 0, 0, 18, 18, 18, 0, 22, 22, 0, 21, 21, 21, 21, 0, 0, 23, 0, 18, 18, 0, 18, 18, 0, 0, 24, 0, 21, 21, 0] - } - }, - { - "id": "small_offset_100", - "plan": "small_offset_100", - "mask": { - "dtype": "uint8", - "shape": [24, 14], - "data": [0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1] - }, - "write": { - "dtype": "int32", - "shape": [7, 12], - "data": [0, 0, 0, 0, 1, 0, 0, 0, 2, 2, 0, 0, 3, 0, 0, 0, 1, 0, 4, 4, 4, 2, 0, 5, 6, 6, 0, 0, 0, 4, 4, 4, 0, 0, 0, 7, 0, 8, 0, 0, 4, 4, 4, 9, 0, 10, 0, 7, 11, 12, 13, 0, 0, 14, 0, 0, 0, 15, 0, 0, 11, 0, 0, 16, 0, 14, 14, 0, 15, 15, 15, 0, 17, 0, 0, 16, 0, 14, 14, 0, 15, 15, 0, 18] - } - }, - { - "id": "small_z_line", - "plan": "small_z", - "mask": { - "dtype": "uint8", - "shape": [32, 32], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - "write": { - "dtype": "int32", - "shape": [16, 16], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2] - } - } - ], - "box_voxels": [ - { - "id": "on_grid", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "offset_nm": [32, 32, 32], - "shape_nm": [8, 16, 24], - "volume_shape": [100, 200, 300], - "corner_nm": [0.0, 0.0, 0.0], - "box": { - "lo": [4, 4, 4], - "hi": [5, 6, 7] - } - }, - { - "id": "centres_decide", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "offset_nm": [30, 36, 44], - "shape_nm": [12, 8, 8], - "volume_shape": [100, 200, 300], - "corner_nm": [0.0, 0.0, 0.0], - "box": { - "lo": [4, 4, 5], - "hi": [5, 5, 6] - } - }, - { - "id": "clipped", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "offset_nm": [-20, 1580, 2380], - "shape_nm": [40, 40, 40], - "volume_shape": [100, 200, 300], - "corner_nm": [0.0, 0.0, 0.0], - "box": { - "lo": [0, 197, 297], - "hi": [2, 200, 300] - } - }, - { - "id": "outside", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "offset_nm": [-100, 0, 0], - "shape_nm": [40, 40, 40], - "volume_shape": [100, 200, 300], - "corner_nm": [0.0, 0.0, 0.0], - "box": null - }, - { - "id": "offset_100", - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "input_voxel_size": [4.0, 4.0, 4.0], - "dataset_offset_nm": [100.0, 100.0, 100.0] - }, - "offset_nm": [96, 100, 104], - "shape_nm": [8, 8, 8], - "volume_shape": [100, 200, 300], - "corner_nm": [96.0, 96.0, 96.0], - "box": { - "lo": [0, 0, 1], - "hi": [1, 1, 2] - } - }, - { - "id": "aniso", - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "input_voxel_size": [8.0, 4.0, 4.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "offset_nm": [0, 0, 0], - "shape_nm": [33, 17, 15], - "volume_shape": [100, 200, 300], - "corner_nm": [0.0, 0.0, 0.0], - "box": { - "lo": [0, 0, 0], - "hi": [2, 2, 2] - } - } - ], - "fresh_ids": [ - { - "id": "lowest_free", - "existing": { - "dtype": "uint8", - "shape": [6], - "data": [0, 0, 1, 2, 3, 5] - }, - "count": 3, - "count_up": false, - "ids": [4, 6, 7] - }, - { - "id": "count_up", - "existing": { - "dtype": "uint8", - "shape": [6], - "data": [0, 0, 1, 2, 3, 5] - }, - "count": 3, - "count_up": true, - "ids": [6, 7, 8] - }, - { - "id": "count_up_from_1", - "existing": { - "dtype": "uint8", - "shape": [4], - "data": [0, 0, 0, 0] - }, - "count": 2, - "count_up": true, - "ids": [2, 3] - }, - { - "id": "uint16_count_up", - "existing": { - "dtype": "uint16", - "shape": [3], - "data": [0, 300, 7] - }, - "count": 2, - "count_up": true, - "ids": [301, 302] - }, - { - "id": "uint16_lowest_free", - "existing": { - "dtype": "uint16", - "shape": [3], - "data": [0, 300, 7] - }, - "count": 2, - "count_up": false, - "ids": [2, 3] - }, - { - "id": "none", - "existing": { - "dtype": "uint8", - "shape": [2], - "data": [0, 2] - }, - "count": 0, - "count_up": false, - "ids": [] - }, - { - "id": "none_count_up", - "existing": { - "dtype": "uint8", - "shape": [2], - "data": [0, 2] - }, - "count": 0, - "count_up": true, - "ids": [] - }, - { - "id": "uint8_full", - "existing": { - "dtype": "uint8", - "shape": [254], - "data": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255] - }, - "count": 1, - "count_up": false, - "error": "1 new objects do not fit in the volume's uint8 labels: make a new annotation volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min size or the threshold so the seed makes fewer objects" - }, - { - "id": "uint8_last_free", - "existing": { - "dtype": "uint8", - "shape": [253], - "data": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254] - }, - "count": 1, - "count_up": false, - "ids": [255] - }, - { - "id": "uint8_count_up_overflow", - "existing": { - "dtype": "uint8", - "shape": [1], - "data": [255] - }, - "count": 1, - "count_up": true, - "error": "1 new objects do not fit in the volume's uint8 labels: make a new annotation volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min size or the threshold so the seed makes fewer objects" - }, - { - "id": "uint16_count_up_overflow", - "existing": { - "dtype": "uint16", - "shape": [1], - "data": [65534] - }, - "count": 2, - "count_up": true, - "error": "2 new objects do not fit in the volume's uint16 labels: make a new annotation volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min size or the threshold so the seed makes fewer objects" - } - ], - "seed_labels": [ - { - "id": "fresh", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [2, 2, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 3, 3, 3] - } - }, - { - "id": "fresh_count_up", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count_up": true, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [2, 2, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 3, 3, 3] - } - }, - { - "id": "painted_kept", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [7, 7, 1, 1, 1, 1, 7, 7, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2] - } - }, - { - "id": "tie_takes_larger_id", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [4, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [6, 6, 1, 1, 1, 1, 6, 6, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 3, 3, 3] - } - }, - { - "id": "majority", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [4, 4, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [4, 4, 1, 1, 1, 1, 4, 4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2] - } - }, - { - "id": "skipped_object_id", - "objects": { - "dtype": "int32", - "shape": [2, 5], - "data": [1, 0, 0, 0, 0, 0, 0, 0, 0, 3] - }, - "existing": { - "dtype": "uint8", - "shape": [2, 5], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [2, 5], - "data": [2, 1, 1, 1, 1, 1, 1, 1, 1, 3] - } - }, - { - "id": "uint16_count_up", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint16", - "shape": [4, 6], - "data": [0, 0, 0, 0, 0, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count_up": true, - "labels": { - "dtype": "uint16", - "shape": [4, 6], - "data": [301, 301, 1, 1, 1, 1, 301, 301, 1, 1, 1, 1, 1, 1, 1, 302, 302, 302, 1, 1, 1, 302, 302, 302] - } - }, - { - "id": "same_id_two_objects", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [5, 5, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 1, 1, 1, 5, 5, 5] - } - }, - { - "id": "no_foreground", - "objects": { - "dtype": "int32", - "shape": [3, 3], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "existing": { - "dtype": "uint8", - "shape": [3, 3], - "data": [0, 0, 0, 0, 4, 0, 0, 0, 0] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [3, 3], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - { - "id": "background_painted_ignored", - "objects": { - "dtype": "int32", - "shape": [4, 6], - "data": [1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 6], - "data": [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count_up": false, - "labels": { - "dtype": "uint8", - "shape": [4, 6], - "data": [2, 2, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 3, 3, 3] - } - } - ], - "labels_for_box": [ - { - "id": "each_object_an_id_axis0", - "objects": { - "dtype": "int32", - "shape": [6, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 3] - }, - "existing": { - "dtype": "uint8", - "shape": [1, 6, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 0, - "count_up": false, - "beside": null, - "labels": { - "dtype": "uint8", - "shape": [1, 6, 8], - "data": [2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 4] - } - }, - { - "id": "each_object_an_id_axis1", - "objects": { - "dtype": "int32", - "shape": [6, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 3] - }, - "existing": { - "dtype": "uint8", - "shape": [6, 1, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 1, - "count_up": false, - "beside": null, - "labels": { - "dtype": "uint8", - "shape": [6, 1, 8], - "data": [2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 4] - } - }, - { - "id": "each_object_an_id_axis2", - "objects": { - "dtype": "int32", - "shape": [6, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 3] - }, - "existing": { - "dtype": "uint8", - "shape": [6, 8, 1], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 2, - "count_up": false, - "beside": null, - "labels": { - "dtype": "uint8", - "shape": [6, 8, 1], - "data": [2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 4] - } - }, - { - "id": "painted_kept_axis0", - "objects": { - "dtype": "int32", - "shape": [6, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "existing": { - "dtype": "uint8", - "shape": [1, 6, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9] - }, - "depth_axis": 0, - "count_up": false, - "beside": null, - "labels": { - "dtype": "uint8", - "shape": [1, 6, 6], - "data": [1, 1, 1, 1, 1, 1, 1, 7, 7, 7, 1, 1, 1, 7, 7, 7, 1, 1, 1, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - { - "id": "count_up_axis1", - "objects": { - "dtype": "int32", - "shape": [4, 8], - "data": [1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0] - }, - "existing": { - "dtype": "uint16", - "shape": [4, 1, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 1, - "count_up": true, - "beside": null, - "labels": { - "dtype": "uint16", - "shape": [4, 1, 8], - "data": [301, 301, 1, 1, 302, 302, 1, 1, 301, 301, 1, 1, 302, 302, 1, 1, 301, 301, 1, 1, 302, 302, 1, 1, 301, 301, 1, 1, 302, 302, 1, 1] - } - }, - { - "id": "continues_beside_axis0", - "objects": { - "dtype": "int32", - "shape": [6, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "existing": { - "dtype": "uint16", - "shape": [1, 6, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 0, - "count_up": true, - "beside": [ - { - "dtype": "uint16", - "shape": [1, 6, 12], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - null - ], - "labels": { - "dtype": "uint16", - "shape": [1, 6, 12], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - { - "id": "continues_beside_axis1", - "objects": { - "dtype": "int32", - "shape": [6, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "existing": { - "dtype": "uint16", - "shape": [6, 1, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 1, - "count_up": true, - "beside": [ - { - "dtype": "uint16", - "shape": [6, 1, 12], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - null - ], - "labels": { - "dtype": "uint16", - "shape": [6, 1, 12], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - { - "id": "continues_beside_axis2", - "objects": { - "dtype": "int32", - "shape": [6, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "existing": { - "dtype": "uint16", - "shape": [6, 12, 1], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 2, - "count_up": true, - "beside": [ - { - "dtype": "uint16", - "shape": [6, 12, 1], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 1, 1, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - null - ], - "labels": { - "dtype": "uint16", - "shape": [6, 12, 1], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 3, 3, 3, 1, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - { - "id": "skips_ids_beside_axis0", - "objects": { - "dtype": "int32", - "shape": [4, 8], - "data": [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0] - }, - "existing": { - "dtype": "uint8", - "shape": [1, 4, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 0, - "count_up": false, - "beside": [ - null, - { - "dtype": "uint8", - "shape": [1, 4, 8], - "data": [2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0] - } - ], - "labels": { - "dtype": "uint8", - "shape": [1, 4, 8], - "data": [1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 3, 3, 1] - } - }, - { - "id": "first_plane_wins_axis2", - "objects": { - "dtype": "int32", - "shape": [4, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "existing": { - "dtype": "uint8", - "shape": [4, 8, 1], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "depth_axis": 2, - "count_up": false, - "beside": [ - { - "dtype": "uint8", - "shape": [4, 8, 1], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - { - "dtype": "uint8", - "shape": [4, 8, 1], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 8, 8, 0, 0, 6, 6, 0, 0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - ], - "labels": { - "dtype": "uint8", - "shape": [4, 8, 1], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 1, 1, 8, 8, 1, 1, 5, 5, 1, 1, 8, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - } - ], - "paint_box": [ - { - "id": "seed", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [0, 0, 0], - "hi": [4, 4, 4], - "labels": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1] - }, - "overwrite": false, - "counts": { - "n_foreground": 28, - "n_background": 32, - "n_overwritten": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 2, 2, 0, 0, 0, 0, 5, 5, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "after": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 2, 2, 5, 5, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - "restore": { - "stroke": [ - [[0, 2, 0], 9], - [[0, 0, 3], 9], - [[6, 6, 6], 9] - ], - "restored": 58, - "final": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 9, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - }, - { - "id": "seed_overwrite", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [0, 0, 0], - "hi": [4, 4, 4], - "labels": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1] - }, - "overwrite": true, - "counts": { - "n_foreground": 32, - "n_background": 32, - "n_overwritten": 4 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "after": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - "restore": { - "stroke": [ - [[0, 1, 0], 9] - ], - "restored": 63, - "final": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 9, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - }, - { - "id": "across_chunks", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [1, 2, 3], - "hi": [5, 6, 7], - "labels": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - "overwrite": false, - "counts": { - "n_foreground": 7, - "n_background": 55, - "n_overwritten": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 3, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7] - }, - "after": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 7] - } - }, - "restore": { - "stroke": [ - [[2, 2, 2], 1] - ], - "restored": 62, - "final": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - }, - { - "id": "across_chunks_overwrite", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [1, 2, 3], - "hi": [5, 6, 7], - "labels": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - "overwrite": true, - "counts": { - "n_foreground": 8, - "n_background": 56, - "n_overwritten": 2 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 3, 3, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7] - }, - "after": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - } - }, - { - "id": "nothing_changes", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [0, 0, 0], - "hi": [4, 4, 4], - "labels": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "overwrite": false, - "counts": { - "n_foreground": 0, - "n_background": 0, - "n_overwritten": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": null - }, - { - "id": "unchanged_labels", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [0, 0, 0], - "hi": [4, 4, 4], - "labels": { - "dtype": "uint8", - "shape": [4, 4, 4], - "data": [1, 1, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "overwrite": true, - "counts": { - "n_foreground": 0, - "n_background": 0, - "n_overwritten": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": null - }, - { - "id": "uint16", - "dtype": "uint16", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 300, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [0, 0, 0], - "hi": [4, 4, 8], - "labels": { - "dtype": "uint16", - "shape": [4, 4, 8], - "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - }, - "overwrite": false, - "counts": { - "n_foreground": 60, - "n_background": 64, - "n_overwritten": 0 - }, - "after": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [1, 1, 2, 2, 2, 2, 2, 2, 300, 300, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint16", - "shape": [4, 4, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 300, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "after": { - "dtype": "uint16", - "shape": [4, 4, 8], - "data": [1, 1, 2, 2, 2, 2, 2, 2, 300, 300, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - } - }, - "restore": { - "stroke": [ - [[0, 3, 7], 400] - ], - "restored": 123, - "final": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [1, 1, 0, 0, 0, 0, 0, 0, 300, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - } - ], - "prompts": { - "max_prompt_chars": 4000, - "editable": { - "mito": "This is an EM image of cell(s). In EM, mitochondria appear as: large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol. Create a segmentation mask: color all the mitochondria in bright red and make everything else black.", - "er": "This is an EM image of cell(s). In EM, endoplasmic reticulum appear as: an extensive, interconnected network of flattened cisternae and tubules (30-60 nm lumen width) that pervades the cytoplasm. Rough ER appears as parallel membrane pairs studded with dark ribosome dots on the cytoplasmic face; smooth ER lacks ribosomes and forms more tubular profiles. ER tubules always connect back to themselves and ultimately connect to the nuclear envelope. Unlike similar-looking multivesicular bodies or vesicles, ER is never disconnected—it forms a single continuous network. Create a segmentation mask: color all the endoplasmic reticulum in bright green and make everything else black.", - "nucleus": "This is an EM image of cell(s). In EM, nucleus appear as: the largest organelle (5-15 µm diameter), roughly spherical or ovoid, bounded by a double-membrane nuclear envelope perforated with nuclear pores. The interior contains chromatin: dark, electron-dense heterochromatin clusters (often along the envelope periphery) and lighter, diffuse euchromatin. A dense, spherical nucleolus (1-5 µm) is often visible as the darkest sub-nuclear structure. The nucleoplasm between chromatin regions appears as a relatively uniform, light gray matrix. Create a segmentation mask: color all the nucleus in bright blue and make everything else black.", - "lipid_droplet": "This is an EM image of cell(s). In EM, lipid droplets appear as: spherical organelles (100 nm-5 µm) enclosed by a lipid monolayer (not a bilayer). In heavy-metal stained EM they appear with a shriveled, lumpy morphology and a generally lighter, more homogeneous interior compared to the surrounding cytosol. Their boundary shows subtle membrane staining. Distinguished from lysosomes and endosomes by their lighter, more uniform interior and absence of internal vesicles or dense granular content. Create a segmentation mask: color all the lipid droplets in bright yellow and make everything else black.", - "plasma_membrane": "This is an EM image of cell(s). In EM, plasma membrane appear as: a thin (~7-8 nm) dark line delineating the outer boundary of the cell, separating extracellular space from the cytosol. It appears as a continuous electron-dense bilayer that follows the entire cell contour, including microvilli, filopodia, and invaginations. It always forms a closed boundary around a cell. If a membrane segment appears disconnected from the cell surface, it is likely part of the endosomal network instead. Create a segmentation mask: color all the plasma membrane in bright cyan and make everything else black.", - "nuclear_envelope": "This is an EM image of cell(s). In EM, nuclear envelope appear as: a double-membrane structure (~30-50 nm total thickness) consisting of two parallel lipid bilayers (inner and outer nuclear membranes) separated by the perinuclear space. The outer membrane is often studded with ribosomes, making it continuous with rough ER. It forms a closed boundary around the nucleus and is perforated by ~120 nm nuclear pore complexes visible as gaps or electron-dense ring structures. It separates the chromatin-containing nucleoplasm from the cytosol. Create a segmentation mask: color all the nuclear envelope in bright magenta and make everything else black.", - "nuclear_pore": "This is an EM image of cell(s). In EM, nuclear pores appear as: ~120 nm diameter protein complexes that perforate the nuclear envelope. In en-face views they appear as ring-shaped electron-dense structures; in cross-section they appear as breaks or gaps in envelope connectivity where the inner and outer nuclear membranes fuse. A central plug or transporter is sometimes visible. They span both bilayers of the nuclear envelope and are distributed across the entire nuclear surface. Create a segmentation mask: color all the nuclear pores in bright orange and make everything else black.", - "nucleolus": "This is an EM image of cell(s). In EM, nucleolus appear as: a large (1-5 µm), dense, roughly spherical sub-nuclear body that is the most electron-dense structure within the nucleus. It has a granular and fibrillar internal texture with distinct sub-compartments: a dense fibrillar component, a granular component, and sometimes fibrillar centers (lighter regions). It stains significantly darker than surrounding chromatin and nucleoplasm. Not bounded by a membrane. Create a segmentation mask: color all the nucleolus in bright purple and make everything else black.", - "heterochromatin": "This is an EM image of cell(s). In EM, heterochromatin appear as: dark, electron-dense, compact clusters of tightly packed chromatin within the nucleus. Typically found along the inner surface of the nuclear envelope (peripheral heterochromatin) and around the nucleolus (perinucleolar heterochromatin). Stains significantly darker and is more compact than the diffuse euchromatin regions. Excludes the nucleolus itself, which has a distinct granular/fibrillar texture. Create a segmentation mask: color all the heterochromatin in bright spring green and make everything else black.", - "euchromatin": "This is an EM image of cell(s). In EM, euchromatin appear as: light, diffuse, loosely packed chromatin that fills much of the nuclear interior between heterochromatin clusters. Appears as a lighter gray, more uniform matrix compared to the dark heterochromatin. Represents transcriptionally active chromatin regions. Located outside of the nucleolus and distinct from the dense nucleolar sub-structure. Excludes chromatin directly associated with the nucleolus. Create a segmentation mask: color all the euchromatin in bright rose and make everything else black.", - "cell": "This is an EM image of cell(s). In EM, cells appear as: whole cells bounded by a plasma membrane. Each cell contains cytoplasm filled with various organelles (mitochondria, ER, nucleus, etc.) and a relatively uniform cytosolic matrix. Cell boundaries are defined by the plasma membrane—a thin dark line separating one cell from another or from extracellular space. Extracellular regions may appear lighter or contain extracellular matrix material. Create a segmentation mask: color all the cells in bright red and make everything else black." - }, - "editable_generic": "This is an EM image of cell(s). Create a segmentation mask: color all the golgi in bright red and make everything else black.", - "cases": [ - { - "id": "mito_8", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 8.0, - "prompt_override": null, - "prompt": "The image resolution is 8 nm/px. This is an EM image of cell(s). In EM, mitochondria appear as: large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol. Create a segmentation mask: color all the mitochondria in bright red and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "mito_3_5", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 3.5, - "prompt_override": null, - "prompt": "The image resolution is 3.5 nm/px. This is an EM image of cell(s). In EM, mitochondria appear as: large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol. Create a segmentation mask: color all the mitochondria in bright red and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "mito_4", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 4.0, - "prompt_override": null, - "prompt": "The image resolution is 4 nm/px. This is an EM image of cell(s). In EM, mitochondria appear as: large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol. Create a segmentation mask: color all the mitochondria in bright red and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "er_12_3456", - "profile": { - "key": "er", - "name": "endoplasmic reticulum", - "color_name": "bright green", - "rgb": [0, 255, 0], - "description": "an extensive, interconnected network of flattened cisternae and tubules (30-60 nm lumen width) that pervades the cytoplasm. Rough ER appears as parallel membrane pairs studded with dark ribosome dots on the cytoplasmic face; smooth ER lacks ribosomes and forms more tubular profiles. ER tubules always connect back to themselves and ultimately connect to the nuclear envelope. Unlike similar-looking multivesicular bodies or vesicles, ER is never disconnected—it forms a single continuous network." - }, - "resolution_nm": 12.3456, - "prompt_override": null, - "prompt": "The image resolution is 12.3 nm/px. This is an EM image of cell(s). In EM, endoplasmic reticulum appear as: an extensive, interconnected network of flattened cisternae and tubules (30-60 nm lumen width) that pervades the cytoplasm. Rough ER appears as parallel membrane pairs studded with dark ribosome dots on the cytoplasmic face; smooth ER lacks ribosomes and forms more tubular profiles. ER tubules always connect back to themselves and ultimately connect to the nuclear envelope. Unlike similar-looking multivesicular bodies or vesicles, ER is never disconnected—it forms a single continuous network. Create a segmentation mask: color all the endoplasmic reticulum in bright green and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "cell_1024", - "profile": { - "key": "cell", - "name": "cells", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "whole cells bounded by a plasma membrane. Each cell contains cytoplasm filled with various organelles (mitochondria, ER, nucleus, etc.) and a relatively uniform cytosolic matrix. Cell boundaries are defined by the plasma membrane—a thin dark line separating one cell from another or from extracellular space. Extracellular regions may appear lighter or contain extracellular matrix material." - }, - "resolution_nm": 1024.0, - "prompt_override": null, - "prompt": "The image resolution is 1.02e+03 nm/px. This is an EM image of cell(s). In EM, cells appear as: whole cells bounded by a plasma membrane. Each cell contains cytoplasm filled with various organelles (mitochondria, ER, nucleus, etc.) and a relatively uniform cytosolic matrix. Cell boundaries are defined by the plasma membrane—a thin dark line separating one cell from another or from extracellular space. Extracellular regions may appear lighter or contain extracellular matrix material. Create a segmentation mask: color all the cells in bright red and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "nucleus_none", - "profile": { - "key": "nucleus", - "name": "nucleus", - "color_name": "bright blue", - "rgb": [0, 0, 255], - "description": "the largest organelle (5-15 µm diameter), roughly spherical or ovoid, bounded by a double-membrane nuclear envelope perforated with nuclear pores. The interior contains chromatin: dark, electron-dense heterochromatin clusters (often along the envelope periphery) and lighter, diffuse euchromatin. A dense, spherical nucleolus (1-5 µm) is often visible as the darkest sub-nuclear structure. The nucleoplasm between chromatin regions appears as a relatively uniform, light gray matrix." - }, - "resolution_nm": null, - "prompt_override": null, - "prompt": "This is an EM image of cell(s). In EM, nucleus appear as: the largest organelle (5-15 µm diameter), roughly spherical or ovoid, bounded by a double-membrane nuclear envelope perforated with nuclear pores. The interior contains chromatin: dark, electron-dense heterochromatin clusters (often along the envelope periphery) and lighter, diffuse euchromatin. A dense, spherical nucleolus (1-5 µm) is often visible as the darkest sub-nuclear structure. The nucleoplasm between chromatin regions appears as a relatively uniform, light gray matrix. Create a segmentation mask: color all the nucleus in bright blue and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "override", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 8.0, - "prompt_override": " Paint the dark blobs red. ", - "prompt": "The image resolution is 8 nm/px. Paint the dark blobs red. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "blank_override", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 8.0, - "prompt_override": " ", - "prompt": "The image resolution is 8 nm/px. This is an EM image of cell(s). In EM, mitochondria appear as: large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol. Create a segmentation mask: color all the mitochondria in bright red and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "generic", - "profile": { - "key": "golgi", - "name": "golgi", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "" - }, - "resolution_nm": 16.0, - "prompt_override": null, - "prompt": "The image resolution is 16 nm/px. This is an EM image of cell(s). Create a segmentation mask: color all the golgi in bright red and make everything else black. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "generic_override", - "profile": { - "key": "golgi", - "name": "golgi", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "" - }, - "resolution_nm": 0.5, - "prompt_override": "Colour every Golgi stack bright red.", - "prompt": "The image resolution is 0.5 nm/px. Colour every Golgi stack bright red. Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - }, - { - "id": "too_long", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 8.0, - "prompt_override": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "error": { - "category": "refused", - "message": "The prompt is longer than 4000 characters.", - "http_status": 400 - } - }, - { - "id": "just_fits", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - "resolution_nm": 8.0, - "prompt_override": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", - "prompt": "The image resolution is 8 nm/px. yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy Where two individual instances are adjacent or touching, leave a thin black border between them so each instance stays visually separate -- never merge touching instances into one connected blob." - } - ] - }, - "organelles": { - "catalog": [ - { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - }, - { - "key": "er", - "name": "endoplasmic reticulum", - "color_name": "bright green", - "rgb": [0, 255, 0], - "description": "an extensive, interconnected network of flattened cisternae and tubules (30-60 nm lumen width) that pervades the cytoplasm. Rough ER appears as parallel membrane pairs studded with dark ribosome dots on the cytoplasmic face; smooth ER lacks ribosomes and forms more tubular profiles. ER tubules always connect back to themselves and ultimately connect to the nuclear envelope. Unlike similar-looking multivesicular bodies or vesicles, ER is never disconnected—it forms a single continuous network." - }, - { - "key": "nucleus", - "name": "nucleus", - "color_name": "bright blue", - "rgb": [0, 0, 255], - "description": "the largest organelle (5-15 µm diameter), roughly spherical or ovoid, bounded by a double-membrane nuclear envelope perforated with nuclear pores. The interior contains chromatin: dark, electron-dense heterochromatin clusters (often along the envelope periphery) and lighter, diffuse euchromatin. A dense, spherical nucleolus (1-5 µm) is often visible as the darkest sub-nuclear structure. The nucleoplasm between chromatin regions appears as a relatively uniform, light gray matrix." - }, - { - "key": "lipid_droplet", - "name": "lipid droplets", - "color_name": "bright yellow", - "rgb": [255, 255, 0], - "description": "spherical organelles (100 nm-5 µm) enclosed by a lipid monolayer (not a bilayer). In heavy-metal stained EM they appear with a shriveled, lumpy morphology and a generally lighter, more homogeneous interior compared to the surrounding cytosol. Their boundary shows subtle membrane staining. Distinguished from lysosomes and endosomes by their lighter, more uniform interior and absence of internal vesicles or dense granular content." - }, - { - "key": "plasma_membrane", - "name": "plasma membrane", - "color_name": "bright cyan", - "rgb": [0, 255, 255], - "description": "a thin (~7-8 nm) dark line delineating the outer boundary of the cell, separating extracellular space from the cytosol. It appears as a continuous electron-dense bilayer that follows the entire cell contour, including microvilli, filopodia, and invaginations. It always forms a closed boundary around a cell. If a membrane segment appears disconnected from the cell surface, it is likely part of the endosomal network instead." - }, - { - "key": "nuclear_envelope", - "name": "nuclear envelope", - "color_name": "bright magenta", - "rgb": [255, 0, 255], - "description": "a double-membrane structure (~30-50 nm total thickness) consisting of two parallel lipid bilayers (inner and outer nuclear membranes) separated by the perinuclear space. The outer membrane is often studded with ribosomes, making it continuous with rough ER. It forms a closed boundary around the nucleus and is perforated by ~120 nm nuclear pore complexes visible as gaps or electron-dense ring structures. It separates the chromatin-containing nucleoplasm from the cytosol." - }, - { - "key": "nuclear_pore", - "name": "nuclear pores", - "color_name": "bright orange", - "rgb": [255, 128, 0], - "description": "~120 nm diameter protein complexes that perforate the nuclear envelope. In en-face views they appear as ring-shaped electron-dense structures; in cross-section they appear as breaks or gaps in envelope connectivity where the inner and outer nuclear membranes fuse. A central plug or transporter is sometimes visible. They span both bilayers of the nuclear envelope and are distributed across the entire nuclear surface." - }, - { - "key": "nucleolus", - "name": "nucleolus", - "color_name": "bright purple", - "rgb": [128, 0, 255], - "description": "a large (1-5 µm), dense, roughly spherical sub-nuclear body that is the most electron-dense structure within the nucleus. It has a granular and fibrillar internal texture with distinct sub-compartments: a dense fibrillar component, a granular component, and sometimes fibrillar centers (lighter regions). It stains significantly darker than surrounding chromatin and nucleoplasm. Not bounded by a membrane." - }, - { - "key": "heterochromatin", - "name": "heterochromatin", - "color_name": "bright spring green", - "rgb": [0, 255, 128], - "description": "dark, electron-dense, compact clusters of tightly packed chromatin within the nucleus. Typically found along the inner surface of the nuclear envelope (peripheral heterochromatin) and around the nucleolus (perinucleolar heterochromatin). Stains significantly darker and is more compact than the diffuse euchromatin regions. Excludes the nucleolus itself, which has a distinct granular/fibrillar texture." - }, - { - "key": "euchromatin", - "name": "euchromatin", - "color_name": "bright rose", - "rgb": [255, 0, 128], - "description": "light, diffuse, loosely packed chromatin that fills much of the nuclear interior between heterochromatin clusters. Appears as a lighter gray, more uniform matrix compared to the dark heterochromatin. Represents transcriptionally active chromatin regions. Located outside of the nucleolus and distinct from the dense nucleolar sub-structure. Excludes chromatin directly associated with the nucleolus." - }, - { - "key": "cell", - "name": "cells", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "whole cells bounded by a plasma membrane. Each cell contains cytoplasm filled with various organelles (mitochondria, ER, nucleus, etc.) and a relatively uniform cytosolic matrix. Cell boundaries are defined by the plasma membrane—a thin dark line separating one cell from another or from extracellular space. Extracellular regions may appear lighter or contain extracellular matrix material." - } - ], - "lookups": [ - { - "name": "mito", - "key": "mito" - }, - { - "name": "Mitochondria", - "key": "mito" - }, - { - "name": " nuclear-pores ", - "key": "nuclear_pore" - }, - { - "name": "lipid_droplet", - "key": "lipid_droplet" - }, - { - "name": "Cell Membrane", - "key": "plasma_membrane" - }, - { - "name": "ENDOPLASMIC RETICULUM", - "key": "er" - }, - { - "name": "nuclear_pore", - "key": "nuclear_pore" - }, - { - "name": "Nucleolus", - "key": "nucleolus" - }, - { - "name": "cells", - "key": "cell" - }, - { - "name": "cell", - "key": "cell" - }, - { - "name": "Lipid Droplet", - "key": "lipid_droplet" - }, - { - "name": "golgi", - "key": null - }, - { - "name": "", - "key": null - }, - { - "name": "mito-chondria", - "key": null - }, - { - "name": "plasma-membrane", - "key": "plasma_membrane" - } - ], - "resolved": [ - { - "name": "golgi", - "profile": { - "key": "golgi", - "name": "golgi", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "" - } - }, - { - "name": " golgi apparatus ", - "profile": { - "key": "golgi apparatus", - "name": "golgi apparatus", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "" - } - }, - { - "name": "Mito", - "profile": { - "key": "mito", - "name": "mitochondria", - "color_name": "bright red", - "rgb": [255, 0, 0], - "description": "large (500-2000 nm), membrane-bound organelles, typically ovoid or elongated (can fuse/branch into tubular networks). Moderate electron density -- darker than the surrounding cytosol but lighter than ribosomes. Exact internal texture varies by cell type, prep, and imaging resolution, so judge primarily by shape, membrane boundary, and density relative to the surrounding cytosol." - } - } - ] - } -} diff --git a/web/test/fixtures/label_it.json b/web/test/fixtures/label_it.json index d1ff2776..89d97345 100644 --- a/web/test/fixtures/label_it.json +++ b/web/test/fixtures/label_it.json @@ -1,1041 +1,114 @@ { - "_format": "Parity fixture for web/src/label-it (segment.ts, prediction.ts) and the Label it parts of web/src/ai-annotate (labels.ts relabelObjects, fill.ts rewriteForeground) against dashboard/routes/finetune/view_labels.py and finetune/session/fill.py; arrays are {dtype, shape, data} flat in C order; see make_label_it_fixtures.py's docstring", + "_format": "Parity fixture for web/src/label-it/segment.ts (mutexObjects) against dashboard/routes/finetune/view_labels.py (segment_prediction); arrays are {dtype, shape, data} flat in C order; see make_label_it_fixtures.py's docstring", "seed": 20261016, - "blocks": { - "aff": { - "dtype": "float32", - "shape": [3, 6, 10, 12], - "data": [-2.408012866973877, -1.8616868257522583, -7.635395526885986, -13.877958297729492, -12.95276927947998, -5.208587646484375, -1.38640558719635, 1.7903149127960205, -1.2399821281433105, -3.5570898056030273, 0.4825134575366974, 6.932269096374512, -1.878836989402771, -2.5423800945281982, -7.156400203704834, -11.827939987182617, -10.52513599395752, -4.752875328063965, -0.20037566125392914, 3.518015146255493, -0.6559981107711792, -4.713518142700195, -4.774991035461426, 1.3531550168991089, 0.08632740378379822, -0.2873893678188324, -2.9489598274230957, -6.207070350646973, -5.882480144500732, -3.2957327365875244, -0.008650673553347588, 2.260319232940674, 1.6773402690887451, -4.198099136352539, -9.582489967346191, -8.854789733886719, 2.5346007347106934, 2.4232444763183594, 3.8571290969848633, 1.8806538581848145, -0.9080127477645874, -1.6726638078689575, -1.128609538078308, -0.3679780960083008, 1.0840394496917725, -4.251401901245117, -8.864547729492188, -9.464752197265625, 4.205121040344238, 6.498582363128662, 5.983445167541504, 4.691836833953857, 0.9092448353767395, -1.565757155418396, -3.526571273803711, -2.0023980140686035, -2.041409730911255, -2.448741912841797, -6.470699787139893, -4.520984649658203, 7.308358192443848, 7.041101455688477, 4.2826151847839355, 4.431109428405762, 2.019653081893921, -1.505902886390686, -2.5404820442199707, -3.437645196914673, -3.7712364196777344, -1.9426356554031372, -3.731736421585083, -2.230139970779419, 5.855698108673096, 2.219977378845215, 0.8422386646270752, 2.8612444400787354, 1.5978503227233887, -0.49994632601737976, -1.4891345500946045, -1.9836140871047974, -0.4414757192134857, 0.010490295477211475, -0.7940539121627808, 0.9179863929748535, -0.44280532002449036, -1.7414833307266235, -2.7466297149658203, -0.10892762988805771, -0.7319851517677307, -2.4498965740203857, -3.4019975662231445, -1.2484164237976074, 0.478891521692276, 1.8937135934829712, 2.002204656600952, 1.8973970413208008, -3.8152315616607666, -0.9902042746543884, -0.5049119591712952, 0.2740210294723511, -1.7925289869308472, -5.324053764343262, -4.879943370819092, -3.6522934436798096, -2.9483656883239746, 0.78085857629776, 2.369396686553955, -1.5242278575897217, -5.989684581756592, -0.8737719655036926, 0.12169979512691498, 0.5253922939300537, -1.0804451704025269, -5.178827285766602, -7.157159805297852, -6.16690731048584, -3.7189319133758545, -0.39507627487182617, -0.49723780155181885, -5.756702423095703, -1.2926872968673706, -2.2450902462005615, -6.067618370056152, -9.232556343078613, -7.384973526000977, -0.9231233596801758, 2.7717580795288086, 5.753438949584961, 3.557182788848877, 0.6724780201911926, 0.9547996520996094, 3.075744152069092, 2.5134196281433105, -1.1279152631759644, -5.097076416015625, -8.58381462097168, -7.238470554351807, -2.3105249404907227, 2.415701150894165, 3.8291056156158447, 1.399038553237915, -1.2494252920150757, -4.955889701843262, -2.5822041034698486, 4.676903247833252, 1.4569936990737915, -0.8990744948387146, -3.8973004817962646, -6.59201192855835, -2.5933022499084473, 0.10564602166414261, 1.4901453256607056, 0.26356586813926697, -3.308326482772827, -8.68215560913086, -8.962335586547852, 4.925816535949707, 3.496833086013794, 1.9916749000549316, -1.062009572982788, -3.342275381088257, -3.5969202518463135, -1.4385734796524048, 1.6317381858825684, -0.7195314168930054, -3.630181312561035, -6.498392581939697, -7.775447845458984, 4.139986991882324, 3.2377188205718994, 2.513317584991455, 0.19782991707324982, -2.5754032135009766, -3.9091594219207764, -2.633000135421753, -0.8158193230628967, 1.1566951274871826, -3.3699111938476562, -3.315167188644409, -2.911355495452881, 5.65306282043457, 3.774578332901001, 0.8960899710655212, -1.4950740337371826, -2.4409737586975098, -3.491931676864624, -2.9095447063446045, -1.9261410236358643, -1.9536128044128418, -1.767831802368164, -1.6199203729629517, -0.32314327359199524, 4.7275190353393555, 1.3765779733657837, -1.058110237121582, -1.2790398597717285, -1.4006397724151611, -1.3793468475341797, -2.196967840194702, -1.381744146347046, -1.0545220375061035, -0.7743997573852539, -0.8698506355285645, 1.4238425493240356, 1.5096323490142822, 0.27681148052215576, -0.24762767553329468, -2.0378153324127197, -0.4883653521537781, -2.639963150024414, 0.17349854111671448, 0.2610703110694885, -0.11301618069410324, -0.12834033370018005, 0.33906713128089905, 0.9998425841331482, -2.9096295833587646, 0.11855968832969666, -1.7276252508163452, -1.8087583780288696, -4.172670841217041, -3.1329610347747803, -1.330511450767517, 0.8713371157646179, 1.0558452606201172, -1.5113649368286133, 0.4627872109413147, -0.8983927369117737, -5.320634841918945, -0.39812082052230835, -0.8883559107780457, -2.885042905807495, -4.1322736740112305, -6.153496265411377, -2.2974648475646973, 0.16519398987293243, 0.16537117958068848, -1.045305848121643, -3.3825607299804688, -6.323968410491943, 1.3748456239700317, -0.9535799622535706, -2.1063313484191895, -4.5150017738342285, -1.6948225498199463, 2.5668466091156006, 6.225533485412598, 6.118143081665039, 4.9540534019470215, 2.065312147140503, 1.4971659183502197, -0.05076722055673599, 1.436385154724121, 0.03618601709604263, -1.1991963386535645, -4.467204570770264, -3.029813051223755, -1.4834198951721191, 2.178842782974243, 2.9516968727111816, 0.9171562790870667, -0.38519349694252014, -2.39792537689209, -3.4859371185302734, 4.587899208068848, 2.8799550533294678, 0.15811441838741302, -1.935112476348877, -5.081773281097412, -0.8919796943664551, 0.26276150345802307, 0.5493282079696655, -0.2503962218761444, -2.9403083324432373, -5.059837818145752, -6.982509136199951, 3.4565443992614746, 3.260592222213745, 1.6376891136169434, 0.27371010184288025, -3.3490495681762695, 0.03011694736778736, 0.8113342523574829, 0.9148156046867371, 0.2645130157470703, -4.186401844024658, -4.371677875518799, -3.8997445106506348, 1.4458144903182983, 1.0143866539001465, 0.8379516005516052, -2.3719301223754883, -3.5311150550842285, -2.29702091217041, 0.6219409108161926, 2.055412769317627, 0.38190507888793945, -2.69046688079834, -3.608488082885742, -2.1365160942077637, 0.6679946780204773, 0.7377948760986328, -2.0067291259765625, -4.326206207275391, -4.242980003356934, -3.367865800857544, -0.14040465652942657, 0.9583743810653687, 0.3688513934612274, 0.04906047135591507, -1.3436427116394043, 1.7816976308822632, 2.5500643253326416, 0.949375569820404, -2.434385299682617, -3.7800686359405518, -4.05682373046875, -1.8432610034942627, 0.10129344463348389, 1.3870699405670166, 1.0687017440795898, 0.3752850592136383, -1.0674822330474854, 2.887171983718872, 1.7842657566070557, 0.7760760188102722, -0.48944535851478577, -1.2249788045883179, -1.4999343156814575, -1.0167534351348877, 0.866033673286438, 2.380084753036499, 2.6606180667877197, 0.14945262670516968, -0.644412100315094, 3.2221672534942627, 1.2431977987289429, 1.4605638980865479, 1.0919643640518188, -0.6336677670478821, -1.7275152206420898, -2.4324281215667725, 0.7695914506912231, 1.7879698276519775, 3.2311387062072754, -0.24256151914596558, -0.3437541425228119, -0.025024425238370895, -0.7291739583015442, 0.6315553784370422, -0.10816432535648346, -2.958915948867798, -3.136484384536743, -3.069047451019287, 0.8652668595314026, 2.602689027786255, 3.284048318862915, -0.568511962890625, -2.4153964519500732, -2.307769298553467, 2.8805923461914062, 0.8141385912895203, 0.3300277292728424, -0.08592476695775986, -0.35796087980270386, -0.1824912577867508, 1.4390811920166016, -0.03940313681960106, 0.6915081739425659, -0.7865666747093201, 0.1925482451915741, -2.7016677856445312, 2.0325684547424316, -1.1624504327774048, -0.8539149165153503, 0.1989862620830536, -0.6849005222320557, -0.5338993668556213, -0.1080905869603157, -0.5732724666595459, -0.890920877456665, -1.9490171670913696, -2.642282247543335, -4.173459529876709, 3.9753010272979736, 0.46395444869995117, 0.9021846055984497, 0.4899967610836029, -0.8237878084182739, -1.0419162511825562, 0.7316176891326904, 0.5003388524055481, 0.01818843185901642, -2.007638692855835, -3.4484894275665283, -4.294129848480225, 2.19499135017395, 1.4348669052124023, 0.24524933099746704, 0.1906759887933731, 0.9770830273628235, 1.5771220922470093, 1.6476643085479736, 1.5929971933364868, 0.18957869708538055, -3.3902604579925537, -6.851498603820801, -5.558556079864502, -1.422677993774414, 0.08897055685520172, 0.802247941493988, -0.5669959783554077, 0.836728036403656, 0.8292063474655151, 2.1701009273529053, 2.3906643390655518, -0.10551414638757706, -3.1074891090393066, -4.197336673736572, -3.017547607421875, -4.327115535736084, -2.1507527828216553, -1.6762503385543823, -2.5425682067871094, -3.913633108139038, -0.8971568942070007, 1.4073954820632935, 1.5369551181793213, 1.2841708660125732, -0.3563466966152191, -1.9504470825195312, 0.04518714174628258, -2.2145767211914062, -0.8783331513404846, -1.7458622455596924, -3.5084807872772217, -2.6545233726501465, -0.9304977655410767, 1.2525639533996582, 1.1026709079742432, 1.8812696933746338, -0.11154262721538544, -0.23195108771324158, 2.1174848079681396, 2.202932119369507, 1.764676570892334, -0.15211403369903564, -1.6974411010742188, -2.484133720397949, -0.11514930427074432, 1.3955628871917725, 2.7732291221618652, 2.881662130355835, 1.0269720554351807, 0.8449430465698242, 4.829641819000244, 1.7813531160354614, 2.620523691177368, 1.8328272104263306, 0.7023739814758301, 0.10546842217445374, 0.5395503640174866, 2.682974338531494, 2.5953304767608643, 1.9062448740005493, -0.8835453391075134, 0.10024052858352661, 2.9469752311706543, 0.9882974028587341, 1.4287933111190796, 1.5061436891555786, -1.0212055444717407, -1.1358610391616821, 0.05874320864677429, 3.8443727493286133, 3.7006945610046387, 2.425151824951172, -0.290044367313385, -0.22490960359573364, 1.4730563163757324, 4.850370407104492, 3.3351705074310303, 1.99351966381073, 1.8540480136871338, 1.827121615409851, -0.45670610666275024, -1.7744451761245728, -3.4483542442321777, -0.878017246723175, 0.08305268734693527, -2.6745083332061768, -4.033672332763672, 2.0366199016571045, 0.003591868095099926, -0.47452521324157715, -0.2677862346172333, -1.1835014820098877, -2.8167967796325684, -3.3422632217407227, -2.4628803730010986, -1.1564242839813232, -1.0755544900894165, -3.1527516841888428, -4.189496040344238, 1.1086091995239258, -0.3612523674964905, -0.8328415751457214, -1.8537397384643555, -1.7867404222488403, -4.4692583084106445, -3.0283961296081543, -0.5343671441078186, 0.21513597667217255, -1.6459400653839111, -2.7528040409088135, -4.793973445892334, 0.3088037371635437, 0.6446649432182312, 0.7112430334091187, -0.00592799624428153, -0.9131083488464355, -1.244986653327942, -1.5874361991882324, 0.6409425735473633, 0.9504044055938721, -0.9731709361076355, -4.240022659301758, -6.5531206130981445, -3.444705009460449, 0.18211156129837036, 0.9850708246231079, 0.9794180989265442, 0.7398785352706909, 1.7223551273345947, 0.9750078320503235, -0.11988998204469681, 0.2830882668495178, -1.78781259059906, -3.405500888824463, -2.6709139347076416, -8.408432960510254, -3.4702658653259277, -0.38182252645492554, -1.3152272701263428, -1.891762375831604, -0.8998918533325195, 1.153627634048462, 2.1296427249908447, 1.8878958225250244, 0.11624397337436676, -1.0915024280548096, -0.7661190032958984, -5.6162543296813965, -3.0737357139587402, -2.8247692584991455, -4.578209400177002, -1.3734873533248901, 0.8002805113792419, 2.4044487476348877, 2.3926267623901367, 3.387995958328247, 0.6855934262275696, -0.07920579612255096, 3.500102996826172, 0.08416826277971268, 0.021345695480704308, -0.7565480470657349, -2.775512456893921, 0.8220051527023315, 1.0397790670394897, 2.764968156814575, 1.8308521509170532, 1.9483040571212769, -0.09744361788034439, 1.681512475013733, 5.3549699783325195, 3.2874534130096436, 2.1239829063415527, 0.06765234470367432, -0.7123547792434692, 1.957047700881958, 3.1390254497528076, 2.8789095878601074, 3.0693914890289307, -0.3698550760746002, -1.6552643775939941, -0.22469615936279297, 2.347907781600952, -0.12300371378660202, 0.5305350422859192, -0.8340153098106384, -1.0997892618179321, 0.5677222609519958, 3.594670295715332, 7.028401851654053, 6.661441802978516, -0.5447112917900085, -2.718259811401367, -2.9747374057769775, 1.717912197113037, 5.09805965423584, 3.6582255363464355, 3.7141826152801514, 4.17206335067749, 3.137234687805176, -1.5567125082015991, -5.56090784072876, -3.7376372814178467, 3.0324084758758545, 4.481452465057373, -0.5073956847190857, -4.132309913635254, 2.1526741981506348, 0.37576034665107727, 0.4054720997810364, -0.4425586462020874, -0.05398963391780853, -3.9850409030914307, -4.97428035736084, -2.099151134490967, 2.2111806869506836, 2.423175811767578, -1.1070289611816406, -4.727197170257568, -0.29421889781951904, 0.09768534451723099, -1.444084644317627, -5.1266374588012695, -6.147166728973389, -6.616794109344482, -5.324514865875244, -0.23675990104675293, 1.5399343967437744, 1.9893943071365356, -1.1761916875839233, -3.278278350830078, -0.9953951239585876, 1.1268374919891357, 0.33200782537460327, -3.596050977706909, -5.655341148376465, -6.128906726837158, -4.0228166580200195, 0.2383260875940323, 1.8449904918670654, 2.1293604373931885, -0.39990878105163574, -2.7467448711395264, -5.694247722625732, -2.4414656162261963, 0.9694525003433228, -2.8027374744415283, -3.1314070224761963, -3.8980419635772705, -2.086300849914551, 0.5642678141593933, 2.4294817447662354, 3.174511194229126, 0.3667086362838745, -0.46711525321006775, -10.448118209838867, -6.470962047576904, -0.8867966532707214, -1.1307634115219116, -2.238886833190918, -2.2111313343048096, -0.5141956806182861, -0.11049474030733109, 2.268152952194214, 2.633150815963745, 0.4387180507183075, 3.182011842727661, -8.38351821899414, -6.863169193267822, -3.473404884338379, -1.545333743095398, 1.3255565166473389, 2.735321044921875, 3.834319829940796, 0.8427574038505554, 1.2119213342666626, 1.4583325386047363, 1.6245412826538086, 6.407881259918213, -2.8986318111419678, -2.4812843799591064, -3.774387836456299, -3.0010790824890137, 1.7248233556747437, 3.703768253326416, 1.150551438331604, 0.836574375629425, 1.1714152097702026, -2.2774317264556885, 0.03857279568910599, 4.4215497970581055, 1.8097158670425415, 0.4857075810432434, -3.0405192375183105, -2.784985065460205, 0.9036570191383362, 1.9513543844223022, 1.9794584512710571, 1.1541938781738281, -0.20226329565048218, -4.368834972381592, -3.6582870483398438, 0.28825289011001587, -1.2179462909698486, -1.9823040962219238, -3.6317238807678223, -2.218801736831665, 0.4970402121543884, 2.5038647651672363, 6.133336067199707, 6.740738868713379, -0.1913842409849167, -6.605157375335693, -5.455565452575684, -0.3151713013648987, -3.047438144683838, -2.168273687362671, -8.28881549835205, -13.495522499084473, -12.738025665283203, -5.595563888549805, -1.9703643321990967, -0.01877436228096485, -1.4313061237335205, -4.539525508880615, 0.09271073341369629, 9.854541778564453, -0.8730522990226746, -2.548271417617798, -6.567235469818115, -10.189924240112305, -10.526252746582031, -4.860848426818848, -0.2992582619190216, 1.909249186515808, 0.256858229637146, -4.779841899871826, -5.566494941711426, 1.2873605489730835, 1.1697895526885986, -0.3921550512313843, -2.7408010959625244, -5.27830171585083, -6.780760288238525, -3.4639503955841064, -0.7437496185302734, 2.2279372215270996, 0.8323553204536438, -4.2174859046936035, -8.761674880981445, -9.031752586364746, 3.051494598388672, 3.265363931655884, 3.871880054473877, 1.451218605041504, 0.0718877762556076, -2.5943257808685303, -1.261685848236084, 0.6179161071777344, -0.10548149049282074, -5.359250068664551, -9.420022010803223, -10.512530326843262, 5.761762619018555, 6.333035945892334, 5.683603286743164, 3.3408613204956055, 1.5385526418685913, -1.8143283128738403, -1.2615631818771362, -2.5827128887176514, -3.0956530570983887, -4.392909526824951, -6.393577575683594, -5.734960556030273, 7.519164562225342, 7.3693060874938965, 4.902182102203369, 4.4964399337768555, 2.210285186767578, -1.546462893486023, -3.3279154300689697, -3.760341167449951, -3.710085153579712, -2.7234368324279785, -3.2868709564208984, -2.4034430980682373, 4.017801761627197, 2.0136072635650635, 0.6062884330749512, 1.8936711549758911, 1.389910101890564, -0.2153027355670929, -1.948790192604065, -2.49601411819458, -0.5029746890068054, -0.6213790774345398, 0.01053889561444521, 0.3115140199661255, -0.1572626233100891, -0.18813014030456543, -1.311647891998291, -1.203264832496643, -0.5883155465126038, -3.5020875930786133, -2.1185097694396973, -1.4304293394088745, 0.603718101978302, 0.880909264087677, 2.2642221450805664, 2.120030164718628, -3.641735553741455, -2.0935401916503906, -1.0475798845291138, -1.012283205986023, -2.908987283706665, -4.684959888458252, -5.281175136566162, -3.7351245880126953, -2.957941770553589, -0.0007909433916211128, 2.2963154315948486, -1.6436834335327148, -5.504324913024902, -0.32074689865112305, -0.003454928519204259, 0.2580948770046234, -1.4793468713760376, -6.084380626678467, -6.4413838386535645, -7.055008888244629, -4.411167144775391, -1.7207244634628296, 0.22761212289333344, -6.36110258102417, -0.6813104748725891, -1.2009583711624146, -6.384050369262695, -10.525700569152832, -7.2192912101745605, -0.5931198000907898, 4.014913082122803, 6.188793182373047, 3.2678098678588867, 0.32864147424697876, 0.6505436897277832, 4.156887531280518, 1.353232502937317, -0.48732805252075195, -4.507816791534424, -8.100707054138184, -6.683464050292969, -1.7229739427566528, 2.513617515563965, 3.069715738296509, 1.3541678190231323, -1.6520992517471313, -4.715150833129883, -4.142140865325928, 4.203893661499023, 2.7897281646728516, -0.9257767796516418, -4.111313819885254, -5.881494045257568, -4.067076206207275, -1.0371553897857666, 2.184033155441284, 1.3384428024291992, -3.2375783920288086, -8.11258316040039, -9.24129867553711, 4.156785011291504, 3.8848094940185547, 2.246568202972412, -0.9895221590995789, -3.2698843479156494, -2.483271360397339, -0.4808250963687897, 0.5829624533653259, -0.21198593080043793, -5.259180545806885, -7.703243732452393, -7.776557445526123, 4.064400672912598, 3.408677339553833, 2.2739782333374023, -0.1931123286485672, -2.579395294189453, -4.959947109222412, -1.5321885347366333, 0.1865449845790863, -0.977641761302948, -2.3744187355041504, -4.779592990875244, -3.615030527114868, 4.765381336212158, 2.8711674213409424, 0.7938101887702942, -0.3653763234615326, -2.198732614517212, -4.7241902351379395, -3.2469687461853027, -2.1993653774261475, -2.8421106338500977, -1.4719942808151245, -1.363568902015686, -0.9657273292541504, 3.2513084411621094, 1.966410756111145, -2.3438401222229004, -1.987823247909546, -2.0567750930786133, -2.409714937210083, -0.6579205393791199, -0.8908036351203918, -0.843920886516571, -0.28334128856658936, 0.1820361614227295, 1.7619640827178955, 0.7791933417320251, 1.0572384595870972, -0.6887388825416565, -1.2006348371505737, -2.3297884464263916, -2.5050241947174072, -0.43925654888153076, 0.6499152183532715, 1.1873637437820435, -0.4134467840194702, 0.7517333030700684, 0.9503271579742432, -3.2813587188720703, -1.2014073133468628, -0.44808608293533325, -3.312673330307007, -3.3480658531188965, -3.5192770957946777, -1.2454591989517212, 0.01955552212893963, 1.1855638027191162, -0.34577029943466187, -0.6087793111801147, -0.45200327038764954, -3.761678695678711, -1.3396656513214111, -1.2159868478775024, -2.330190420150757, -4.33595085144043, -4.70501708984375, -1.6614019870758057, -0.1355842500925064, 0.017234699800610542, -1.0937198400497437, -1.7852548360824585, -4.9550628662109375, 0.5214547514915466, -0.7618433237075806, -3.1954293251037598, -5.570546627044678, -2.294827461242676, 1.7030833959579468, 5.758100509643555, 6.011662006378174, 4.550583839416504, 2.0934667587280273, 1.808605670928955, -1.1411038637161255, 2.7092926502227783, 1.1885148286819458, -1.5579290390014648, -3.365140438079834, -3.5706892013549805, 1.4747517108917236, 2.3465468883514404, 2.658062696456909, 0.9515602588653564, -0.5963689684867859, -1.5792410373687744, -3.3375725746154785, 4.471005916595459, 2.3780202865600586, 0.19155234098434448, -2.0054638385772705, -4.172901153564453, -2.0422019958496094, 1.8060886859893799, 2.5231590270996094, -0.237338125705719, -2.6377344131469727, -4.176973819732666, -6.632683277130127, 4.655927658081055, 2.464085102081299, 2.586639404296875, -0.9986385107040405, -3.1481194496154785, -1.4545903205871582, 1.8520333766937256, 1.9855456352233887, -0.28604525327682495, -3.6329472064971924, -4.870926856994629, -5.089208126068115, 1.0205594301223755, 0.9676828384399414, -0.14239636063575745, -1.7375680208206177, -3.9522581100463867, -2.5445358753204346, 0.16291233897209167, 2.0383083820343018, 0.07376474887132645, -2.089430332183838, -2.7491540908813477, -0.4595937132835388, 0.28199970722198486, 0.3095923960208893, -0.8815261125564575, -3.8126189708709717, -4.315069675445557, -3.338268995285034, 1.0295692682266235, 0.008405809290707111, 1.297667384147644, -0.7244741916656494, -0.4483046531677246, 1.092297077178955, 1.398335337638855, 0.7907474637031555, -1.0502110719680786, -3.2319023609161377, -4.424368381500244, -2.004925489425659, -0.15282884240150452, 1.0877947807312012, 1.3600575923919678, -0.3006836175918579, -0.564768373966217, 2.6006128787994385, 1.1088275909423828, 1.6900768280029297, 1.6210215091705322, -1.2391693592071533, -2.8124592304229736, -0.9244880080223083, 0.7289460301399231, 2.3326797485351562, 2.1419479846954346, -0.3567676544189453, -0.6818534731864929, 3.5119926929473877, 0.2651408910751343, 2.999838352203369, 1.28799307346344, -0.3871830999851227, -2.6182196140289307, -3.0780675411224365, -0.7699236869812012, 3.62988543510437, 1.129927396774292, -1.3304595947265625, -0.6626585125923157, 1.848245620727539, -1.4756839275360107, 0.4406362473964691, -0.3636194169521332, -3.4902825355529785, -3.955638885498047, -3.5305840969085693, 0.688605785369873, 3.7132086753845215, 3.103642702102661, -1.3036975860595703, -2.810805320739746, -2.745328903198242, 2.8680877685546875, -0.14837746322155, 0.22737878561019897, -0.38461533188819885, 0.19822028279304504, 1.5792219638824463, 1.9992390871047974, 0.5673223733901978, -0.20586365461349487, -0.5467126965522766, -0.23038025200366974, -2.450313091278076, 2.6205482482910156, -0.21616004407405853, -0.75398850440979, -1.4327973127365112, -0.5033822059631348, -0.29701104760169983, -1.5143369436264038, -1.1085870265960693, -0.40857160091400146, -0.44900351762771606, -3.0284698009490967, -4.571092128753662, 3.1448817253112793, 1.1573771238327026, -0.21004250645637512, 0.9219634532928467, -0.6777111887931824, -0.6671938896179199, -0.7932578921318054, 0.38776353001594543, -0.11381608992815018, -2.9959423542022705, -3.982698917388916, -4.852104187011719, 2.18113112449646, 1.362074613571167, 1.268725037574768, 1.1387689113616943, 1.1709853410720825, 1.425374150276184, 2.5212903022766113, 1.2075488567352295, -0.7512452006340027, -4.291980743408203, -5.306008338928223, -5.366443634033203, -0.9340111613273621, 0.16635994613170624, 1.3929921388626099, -0.44346141815185547, -0.35939860343933105, 1.0146942138671875, 2.6500611305236816, 1.894307255744934, 0.24716322124004364, -2.854832649230957, -4.865742206573486, -3.1833438873291016, -4.00129508972168, -1.1195743083953857, -2.8832077980041504, -3.6434242725372314, -3.9103543758392334, -1.3602583408355713, 1.3072389364242554, 0.7175659537315369, 1.350795865058899, -0.8761870861053467, -1.617796778678894, 1.1377942562103271, -2.6431972980499268, -1.5910338163375854, -2.767127513885498, -4.857318878173828, -4.043252944946289, -1.8716459274291992, -0.36657851934432983, 1.958733320236206, 1.6151707172393799, 0.8590632081031799, 0.6223321557044983, 3.2042229175567627, 1.7881869077682495, 1.5804052352905273, -0.09144793450832367, -0.9504727125167847, -0.7356910705566406, 0.04464878514409065, 1.8362388610839844, 2.33559513092041, 2.41155743598938, 1.0835152864456177, 0.16832894086837769, 3.634382963180542, 1.8350998163223267, 3.998826742172241, 1.4340746402740479, 0.08407174050807953, -1.5280908346176147, 0.05528806522488594, 3.4551589488983154, 3.276571035385132, 2.248415946960449, -0.22636295855045319, 0.24948976933956146, 3.737851858139038, -0.19441109895706177, 1.333179235458374, 1.6137789487838745, 0.3803427517414093, -0.6867232322692871, 0.6209784150123596, 4.0310845375061035, 3.6445038318634033, 2.454918384552002, -1.8167779445648193, -0.9096272587776184, 1.2442682981491089, 4.8147196769714355, 2.302786111831665, 2.916531562805176, 2.3910250663757324, 1.3847365379333496, -0.9814068675041199, -1.886003017425537, -3.2588300704956055, -1.3070706129074097, -0.5892320275306702, -2.387781858444214, -4.383433818817139, 3.048870086669922, 0.21360887587070465, -0.7100679874420166, -1.0184634923934937, -0.6289791464805603, -3.4308905601501465, -4.840370178222656, -2.7075514793395996, 0.22795900702476501, 0.46729612350463867, -2.5196070671081543, -4.397587776184082, -0.14516372978687286, 0.5832093358039856, -0.606127917766571, -1.9509055614471436, -2.275235652923584, -3.414339542388916, -3.3429808616638184, -1.8000712394714355, -0.17007087171077728, 0.06861856579780579, -3.5657100677490234, -4.981606960296631, -0.1216333657503128, 0.6008153557777405, 0.7761881947517395, -0.273278146982193, -1.1161280870437622, -1.2107257843017578, -2.7179977893829346, 0.43997031450271606, 0.11434772610664368, -2.2046101093292236, -2.903735637664795, -5.178676128387451, -2.565124273300171, -0.6846389174461365, 2.3488271236419678, 0.0475553460419178, -0.561184287071228, 1.3321056365966797, 0.7064008712768555, 0.30533474683761597, 1.8119362592697144, -0.7564442157745361, -3.6770966053009033, -3.642864227294922, -8.002326011657715, -3.7860960960388184, 0.26202160120010376, -0.9147992730140686, -3.013606309890747, -0.039615921676158905, 1.6543244123458862, 1.5480808019638062, 1.463028907775879, 1.3496562242507935, -0.6811743974685669, 0.3697015643119812, -6.059293746948242, -4.275066375732422, -3.2499520778656006, -1.2423557043075562, -1.425735592842102, 0.9679582715034485, 2.527215003967285, 2.642637252807617, 3.108771324157715, 0.21836721897125244, 0.8767382502555847, 5.117015838623047, 0.8555504679679871, -1.6229912042617798, -1.6632698774337769, -2.1968495845794678, 0.27895334362983704, 1.8918575048446655, 2.132584810256958, 2.277092456817627, 2.0673372745513916, 0.24710120260715485, 0.9291342496871948, 6.253337383270264, 2.6827940940856934, 0.5776730179786682, 0.4424195885658264, 0.07366172224283218, 0.7553902864456177, 3.1838040351867676, 2.971440553665161, 3.019942045211792, 0.9683021306991577, -1.6164453029632568, 0.8794623017311096, 3.14176082611084, 0.564613401889801, 0.6998596787452698, 0.7146958708763123, -0.4593418538570404, 0.560994029045105, 4.0514044761657715, 7.866203308105469, 7.223546028137207, 0.6304177641868591, -2.3485162258148193, -2.5835423469543457, 1.9291683435440063, 5.1600260734558105, 3.7116641998291016, 3.473275661468506, 5.208483695983887, 3.975433588027954, -0.4990801513195038, -5.18727445602417, -3.086184501647949, 2.2125649452209473, 3.4806675910949707, -0.21521030366420746, -4.091070652008057, 1.698250651359558, 1.5951218605041504, -1.5319931507110596, 0.1922118216753006, -0.9872641563415527, -3.889072895050049, -4.1938910484313965, -3.2407097816467285, 1.4879752397537231, 1.8586831092834473, -0.4605787694454193, -3.2467093467712402, -0.08274445682764053, 2.3992817401885986, -1.022424578666687, -3.9646501541137695, -5.870484352111816, -6.106621742248535, -5.609991073608398, -1.3338980674743652, 1.2751109600067139, 0.31938835978507996, -1.0638666152954102, -4.088937282562256, -0.1346305012702942, 1.5653327703475952, -0.11379311978816986, -4.436103343963623, -7.080254077911377, -6.244697570800781, -4.425197601318359, 0.015226801857352257, 2.13871693611145, 2.5394458770751953, 1.049523949623108, -3.4762048721313477, -6.3270368576049805, -2.564084053039551, 0.9617857336997986, -1.0816887617111206, -4.2861738204956055, -3.119234085083008, -3.1896440982818604, 0.7565459609031677, 2.886237621307373, 3.8585622310638428, 0.9062458276748657, -0.7208783030509949, -10.32301139831543, -5.9860734939575195, -1.087932825088501, -0.24572719633579254, -1.1687613725662231, -1.4158588647842407, -0.9025010466575623, 0.0022232220508158207, 1.9068734645843506, 1.8675007820129395, 0.8825194239616394, 3.866624116897583, -8.83193588256836, -5.2056965827941895, -3.6692748069763184, -1.8163501024246216, 1.1431221961975098, 2.9644503593444824, 3.3357927799224854, 1.536330223083496, 2.1849639415740967, 1.7488499879837036, 2.6248908042907715, 4.699020862579346, -2.2138216495513916, -2.860178232192993, -3.9881699085235596, -2.6888740062713623, 2.2840116024017334, 3.6136157512664795, 1.9147781133651733, 0.12169007956981659, 0.17231252789497375, -1.2190055847167969, 0.046574003994464874, 5.188053131103516, 1.1804507970809937, -0.08220671117305756, -2.1310031414031982, -2.731112003326416, 1.269270420074463, 2.4314634799957275, 0.895947277545929, 1.0296870470046997, -1.7704277038574219, -4.565031051635742, -4.02275276184082, -0.7671120166778564, -0.421358585357666, -2.3681318759918213, -3.1413557529449463, -1.315256953239441, -0.014841797761619091, 2.092574119567871, 5.9351396560668945, 6.429358005523682, -0.4119918644428253, -7.205306053161621, -4.257431983947754, -0.05719760060310364, -3.0235207080841064, -2.641233444213867, -8.273048400878906, -14.755550384521484, -11.85234260559082, -6.120910167694092, -1.210567831993103, 1.0304526090621948, -0.922285258769989, -4.454555511474609, 0.45774394273757935, 8.451590538024902, -0.5562089681625366, -2.1775903701782227, -6.926605701446533, -10.664493560791016, -9.738794326782227, -5.0642619132995605, 0.876872718334198, 3.2571957111358643, 0.2266521006822586, -4.5358147621154785, -5.022268772125244, 1.1161319017410278, 1.2227933406829834, -1.6723498106002808, -1.8665204048156738, -4.554764270782471, -5.8539934158325195, -3.6127231121063232, -0.8043027520179749, 2.4507484436035156, 1.2607053518295288, -3.891085386276245, -9.096057891845703, -8.234302520751953, 2.9459991455078125, 2.8261265754699707, 2.8979439735412598, 1.3286869525909424, -0.3912093937397003, -2.996443033218384, -2.0156497955322266, 0.15738807618618011, 0.04448453709483147, -4.0694475173950195, -8.241008758544922, -11.051401138305664, 5.704662799835205, 6.11333703994751, 6.3261613845825195, 3.9242095947265625, 1.6823174953460693, -2.5923945903778076, -2.6217403411865234, -1.0905117988586426, -2.726395845413208, -3.5358946323394775, -6.632088661193848, -6.667128086090088, 7.314389228820801, 6.457969665527344, 5.057374477386475, 4.869076728820801, 2.3640639781951904, -0.5180529952049255, -3.141686201095581, -4.469808101654053, -2.951448678970337, -2.037564516067505, -3.6918652057647705, -2.3427133560180664, 4.967573165893555, 1.6999446153640747, -0.34363695979118347, 2.438483476638794, 2.232098340988159, -1.3128321170806885, -2.3441131114959717, -2.2258458137512207, -0.6979770064353943, -1.6991610527038574, 0.5918304920196533, 0.5147883296012878, -0.3665400743484497, -0.3145156502723694, -2.2719993591308594, 1.000717282295227, -0.8117587566375732, -3.8589258193969727, -2.648578405380249, -0.961097002029419, -0.6624096035957336, 0.7400702834129333, 3.7525875568389893, 1.7168869972229004, -3.9428153038024902, -1.5203421115875244, -0.909915030002594, -1.1649047136306763, -2.2648396492004395, -5.595508098602295, -4.839173793792725, -3.3694398403167725, -2.8233447074890137, 1.4601523876190186, 1.414749026298523, -2.011659622192383, -6.013598918914795, -0.3391501009464264, 0.4399683475494385, -0.39033055305480957, -1.8163883686065674, -5.805940628051758, -7.633303642272949, -7.032828330993652, -5.014503002166748, -1.3540464639663696, -0.20903520286083221, -6.227407455444336, 0.01804462820291519, -2.1771609783172607, -5.630108833312988, -10.961525917053223, -7.53602409362793, -2.503570556640625, 3.2012250423431396, 6.2574567794799805, 3.164293050765991, 1.9288502931594849, 1.884649634361267, 3.8974883556365967, 1.0093610286712646, -0.5002491474151611, -4.704831600189209, -7.605563640594482, -5.788614749908447, -1.8445172309875488, 2.620053768157959, 2.685027837753296, 1.1544458866119385, -1.9276466369628906, -4.638147830963135, -4.099575996398926, 3.6612792015075684, 1.3872511386871338, -0.7544205188751221, -3.3185086250305176, -5.94015645980835, -3.5989794731140137, -0.3727740943431854, 0.8569169044494629, -0.30092930793762207, -3.202531337738037, -7.5333147048950195, -9.150395393371582, 4.273138046264648, 3.332742691040039, 1.967689037322998, -0.8242778182029724, -3.651998281478882, -3.575510263442993, -0.8029270768165588, 2.597411870956421, 0.74796062707901, -4.388062953948975, -7.482674598693848, -7.350734233856201, 4.380152702331543, 3.184969186782837, 1.7559397220611572, -0.01433626376092434, -3.0083730220794678, -3.8065922260284424, -2.4435198307037354, 0.07799739390611649, -0.7909270524978638, -3.23305082321167, -3.462684392929077, -2.9452497959136963, 4.9486403465271, 2.4296910762786865, 0.3391300141811371, -0.3361288607120514, -1.5948734283447266, -3.565587282180786, -2.7667410373687744, -1.8034398555755615, -1.9407954216003418, -0.8306188583374023, -1.3317571878433228, -0.04992975294589996, 4.171689033508301, 2.164907932281494, -1.100140929222107, -1.1196821928024292, -0.9682506918907166, -2.3993003368377686, -2.339451789855957, -0.443074494600296, -0.28117698431015015, -0.19396427273750305, 0.059836357831954956, 1.9545316696166992, 1.8140814304351807, 0.22355009615421295, -1.3680118322372437, -0.9429652094841003, -2.193136692047119, -1.2105673551559448, -1.0208203792572021, 0.8833082914352417, -0.2877214848995209, -0.9025020003318787, 0.7843860387802124, 1.3409724235534668, -1.9572689533233643, -0.7112435698509216, -0.9091284275054932, -1.8898247480392456, -2.969493865966797, -3.0527238845825195, -1.6580731868743896, 0.7987787127494812, -0.08228138834238052, 0.4825117290019989, 0.544969379901886, -2.0934629440307617, -3.4259533882141113, -1.777483582496643, -0.10746374726295471, -2.6419215202331543, -4.962833404541016, -4.528398513793945, -2.373871326446533, 0.9087334871292114, 0.6383858919143677, -0.34704551100730896, -2.8985021114349365, -7.7794508934021, -0.5227267146110535, -0.9286987781524658, -2.6508898735046387, -4.383694648742676, -1.4287258386611938, 2.323607921600342, 4.861857891082764, 5.396994590759277, 4.290916442871094, 2.984405040740967, 1.9547520875930786, -1.1776230335235596, 2.1269071102142334, -0.31062549352645874, -1.3807482719421387, -3.4066295623779297, -3.5053327083587646, 0.4833909869194031, 2.8786864280700684, 2.3832757472991943, 1.1036128997802734, -0.5112663507461548, -2.416079521179199, -4.540359020233154, 5.188281059265137, 3.485337257385254, 0.823918879032135, -1.857440710067749, -3.0265345573425293, -1.198028564453125, 0.7954888939857483, 1.3493775129318237, -0.4636877775192261, -2.5397918224334717, -5.6107258796691895, -6.586819171905518, 4.2996015548706055, 4.136742115020752, 2.1417109966278076, -1.3272273540496826, -2.6429760456085205, -2.225238084793091, 1.4927263259887695, 1.9335691928863525, -0.24541515111923218, -4.623762130737305, -5.224716663360596, -4.453032493591309, 1.2369492053985596, 0.5669635534286499, 1.2094752788543701, -2.356919765472412, -3.9471275806427, -2.3387773036956787, 0.20386910438537598, 1.8273969888687134, 0.5975757837295532, -1.5713139772415161, -2.927382230758667, -2.0545318126678467, -0.029040846973657608, 0.9665926098823547, -1.1198699474334717, -3.791365385055542, -4.416255950927734, -3.5851778984069824, -0.1810365617275238, 0.174178346991539, 0.49407699704170227, 0.3020193576812744, -0.46028900146484375, 1.165331244468689, 2.3737215995788574, 0.793190598487854, -2.821751832962036, -3.327451229095459, -3.5396652221679688, -2.0795176029205322, 0.316110759973526, 1.8023728132247925, 0.5705940127372742, -0.0016771904192864895, -0.5363416075706482, 2.345766544342041, 1.551581621170044, 0.31445300579071045, 0.24871373176574707, -1.0193697214126587, -3.7345120906829834, -0.636695384979248, 1.4175821542739868, 0.8185352087020874, 1.5989911556243896, 0.16871340572834015, 0.23767343163490295, 3.081733465194702, 0.47186991572380066, 1.8062524795532227, 1.3204602003097534, -1.6852623224258423, -3.9146509170532227, -2.7635481357574463, 1.2077940702438354, 2.8347525596618652, 2.3169238567352295, -0.3201342523097992, -1.1920664310455322, -0.0066507738083601, -1.4071674346923828, 0.8230295777320862, -0.7987897396087646, -2.2807042598724365, -3.997506856918335, -3.7647929191589355, -1.0830665826797485, 4.331004619598389, 3.83583927154541, -0.686138391494751, -2.9119679927825928, -4.25607442855835, 3.2237257957458496, 1.3186709880828857, 0.9521510004997253, 0.7706305384635925, 1.3680710792541504, 0.6477935910224915, 1.4972622394561768, 0.39854469895362854, 0.14898709952831268, -0.6265826225280762, -1.3012785911560059, -3.866986036300659, 2.400517463684082, 0.47410905361175537, -0.773917019367218, -0.8694459199905396, -1.2075754404067993, -0.5054649114608765, -0.11041954904794693, -0.674570620059967, -0.4840260148048401, -1.33348548412323, -3.9301905632019043, -4.123629570007324, 2.19814395904541, 1.1520850658416748, 0.5344250798225403, -0.6594778299331665, -0.8814184069633484, -1.0365742444992065, -1.1787911653518677, 0.6060615181922913, -0.6124733686447144, -2.829493284225464, -4.802706241607666, -4.791903972625732, 2.357140302658081, 0.6322171092033386, 0.23850345611572266, 0.7408105731010437, 1.0094718933105469, 1.0048283338546753, 1.8387689590454102, 0.5824020504951477, -0.5294519066810608, -4.667088985443115, -6.007894039154053, -4.491955280303955, -1.1851441860198975, 0.4080335795879364, 0.4994027614593506, -0.7630894184112549, -1.2165573835372925, 1.6738697290420532, 3.161329746246338, 1.7304449081420898, -0.7385091185569763, -2.886620283126831, -3.9196417331695557, -2.4363887310028076, -4.392155170440674, -1.676820158958435, -1.9794508218765259, -3.1894657611846924, -3.416232109069824, -1.3782765865325928, 1.3439990282058716, 1.9911798238754272, 1.6870429515838623, 0.08929463475942612, -2.777203321456909, 0.7521072030067444, -2.8278660774230957, -0.5279102325439453, -2.6683413982391357, -3.632549285888672, -3.5716214179992676, -0.05693662166595459, 2.0991873741149902, 2.0242230892181396, 2.039106607437134, 1.350867509841919, -0.2020774632692337, 2.3209187984466553, 2.6655335426330566, 0.6715124249458313, 0.1949157863855362, -1.7440532445907593, -1.236172080039978, 0.39739298820495605, 2.359036684036255, 2.4856560230255127, 1.2467126846313477, 0.7540059685707092, -0.3851531147956848, 4.395968914031982, 2.66198992729187, 3.220245838165283, 1.0780390501022339, -0.8678834438323975, -0.6479617357254028, 1.3541111946105957, 2.6375503540039062, 2.664640188217163, 0.8429329991340637, -0.44572222232818604, 0.5289144515991211, 3.7875165939331055, 0.4975298047065735, 2.0823450088500977, 2.3411035537719727, -0.4614157974720001, -1.3787782192230225, -0.05092770233750343, 2.6271913051605225, 4.584259510040283, 2.4610800743103027, -2.198118209838867, -0.7816820740699768, 0.7979102730751038, 4.8350934982299805, 4.073448181152344, 1.3364216089248657, 2.6015889644622803, 2.856713056564331, -0.5114399790763855, -3.086521863937378, -2.2422447204589844, -2.301285982131958, 0.07254243642091751, -2.3182640075683594, -4.512198448181152, 2.128556251525879, 0.9756851196289062, -0.7584308981895447, -0.5547942519187927, -1.0637092590332031, -4.513840675354004, -4.135213375091553, -3.133516788482666, 0.7268259525299072, -0.49381735920906067, -3.3074541091918945, -5.774383544921875, 0.5165582895278931, 0.460343599319458, -0.7443336248397827, -1.8302803039550781, -2.071714162826538, -4.311295986175537, -3.2479283809661865, -2.113408088684082, 1.8991776704788208, -0.7100796103477478, -1.9900403022766113, -5.493049144744873, -1.1517424583435059, 0.6801921725273132, 0.5521970987319946, -0.8859362602233887, 0.5588602423667908, -0.22892425954341888, -1.9376120567321777, 1.0426615476608276, 0.6425727605819702, -1.7180992364883423, -4.274832725524902, -5.16631555557251, -2.884481430053711, -0.48496973514556885, 0.4497873783111572, -0.3168115019798279, -0.25072401762008667, 0.9361869692802429, 1.3666491508483887, 0.6588650941848755, 1.0871526002883911, -0.43664494156837463, -2.8956263065338135, -3.51820969581604, -7.443141937255859, -3.814927339553833, -0.31896981596946716, -1.8320190906524658, -1.687697410583496, 0.6938628554344177, 2.57655668258667, 0.364485502243042, 1.365473747253418, 0.08888563513755798, -0.4010237753391266, -0.3640817701816559, -5.536062717437744, -2.6307919025421143, -2.5670132637023926, -1.6128867864608765, -1.1711372137069702, 1.3750427961349487, 1.9645577669143677, 1.0765702724456787, 1.9688366651535034, 0.200858011841774, -0.19517889618873596, 4.84155797958374, 0.7529194951057434, 0.48252466320991516, -1.7270100116729736, -2.1986279487609863, 0.23151308298110962, 2.2368175983428955, 1.4162105321884155, 1.0161023139953613, 2.036745309829712, -0.4427013397216797, 0.653350293636322, 5.407318592071533, 2.234356641769409, 3.939250946044922, -0.23455174267292023, 0.41356098651885986, 0.5914181470870972, 2.719435691833496, 2.3831212520599365, 2.458174705505371, 0.8885786533355713, -1.149174690246582, -1.1886171102523804, 4.569050312042236, -0.5170860886573792, -0.09353344142436981, -0.1580287516117096, -0.3214084506034851, 1.0115028619766235, 2.324822187423706, 6.419041633605957, 6.896543979644775, 1.0265557765960693, -2.980346202850342, -1.158826231956482, 2.0152783393859863, 5.142370700836182, 3.156967878341675, 3.652390241622925, 5.627764701843262, 3.1875765323638916, -0.677075982093811, -5.374860763549805, -3.0990567207336426, 1.6084141731262207, 4.153261184692383, -0.5389736294746399, -3.4175896644592285, 2.5418128967285156, 0.8298355340957642, -1.3611836433410645, -1.415602207183838, -0.5903155207633972, -4.583138465881348, -4.860563278198242, -1.745717167854309, 1.5182757377624512, 2.321664571762085, -1.9023902416229248, -4.043533802032471, 0.16522110998630524, 1.2582850456237793, -0.3341617286205292, -5.07008171081543, -5.329344272613525, -6.579571723937988, -4.570104122161865, -0.8693384528160095, 2.0503807067871094, 1.681522250175476, 1.0526096820831299, -2.3259854316711426, -0.7652273774147034, 1.250269889831543, -0.6203658580780029, -5.291136741638184, -6.271749973297119, -6.758517265319824, -3.9572083950042725, -0.5269044637680054, 2.62518572807312, 1.9250617027282715, 0.6161575317382812, -2.163149118423462, -5.7847161293029785, -1.2854450941085815, 0.5532450079917908, -1.5428986549377441, -4.087197303771973, -5.164933681488037, -1.7387669086456299, 1.3531773090362549, 2.2773079872131348, 4.202962875366211, 1.0233505964279175, -0.7866402864456177, -9.417374610900879, -7.482257843017578, -0.7545852065086365, -1.2614222764968872, -1.0430731773376465, -0.7266314625740051, -1.3018608093261719, 0.34291571378707886, 2.5909039974212646, 3.047797679901123, 0.9682397246360779, 3.0566627979278564, -8.602701187133789, -6.209558486938477, -2.709000587463379, -1.3229467868804932, 1.3975489139556885, 2.615324020385742, 2.3692119121551514, 1.9101688861846924, 0.43746963143348694, 2.1388957500457764, 1.619469165802002, 5.941532135009766, -1.7934848070144653, -2.2771501541137695, -4.365923881530762, -3.457249641418457, 2.596538543701172, 2.514699935913086, 1.80575692653656, 0.5717318058013916, 0.9777266383171082, -1.6942306756973267, -0.31294435262680054, 4.060830593109131, 0.2516641616821289, 0.45826131105422974, -2.6597700119018555, -3.480146646499634, 1.2981382608413696, 2.005169153213501, 1.149321436882019, 1.294904112815857, -0.8855780959129333, -4.52325439453125, -3.3590826988220215, -0.5464231371879578, 0.713046133518219, -1.8356891870498657, -3.9821338653564453, -3.104321002960205, 0.7632315158843994, 1.7845418453216553, 5.286677837371826, 6.117099761962891, -0.9327471852302551, -7.602943420410156, -5.2645769119262695, -0.0046853781677782536] - }, - "aff_unit": { - "dtype": "float32", - "shape": [3, 4, 8, 9], - "data": [0.9922905564308167, 0.9840100407600403, 0.863132894039154, 0.5512760281562805, 0.462267130613327, 0.1105804517865181, 0.029842454940080643, 0.026870736852288246, 0.09833019971847534, 0.42340829968452454, 0.5454261302947998, 0.7167693972587585, 0.5931578874588013, 0.7131946086883545, 0.4964543879032135, 0.06668739765882492, 0.011743040755391121, 0.026716843247413635, 0.03657913953065872, 0.08240184932947159, 0.8980197906494141, 0.8477383255958557, 0.5982571840286255, 0.40480634570121765, 0.027066901326179504, 0.003738345578312874, 0.031850431114435196, 0.6189762949943542, 0.7821342945098877, 0.7769390344619751, 0.8043703436851501, 0.5696537494659424, 0.2195032835006714, 0.017678476870059967, 0.014189555309712887, 0.01739378273487091, 0.9243452548980713, 0.5729891061782837, 0.2985822260379791, 0.4980350136756897, 0.22841674089431763, 0.2253529280424118, 0.07469791918992996, 0.04588291421532631, 0.003031517146155238, 0.3176131546497345, 0.03164485841989517, 0.15728852152824402, 0.5909695625305176, 0.6278124451637268, 0.852327823638916, 0.9513639211654663, 0.10163088142871857, 0.0001025209276122041, 0.6625762581825256, 0.3820666968822479, 0.3413788974285126, 0.9412281513214111, 0.9921530485153198, 0.9998444318771362, 0.9990301728248596, 0.5191853642463684, 0.0011033881455659866, 0.8144502639770508, 0.7096923589706421, 0.9643499851226807, 0.9825302362442017, 0.9980684518814087, 0.9999210238456726, 0.999946653842926, 0.9778551459312439, 0.08518988639116287, 0.9965839982032776, 0.9424138069152832, 0.8875185251235962, 0.43979597091674805, 0.8809612393379211, 0.9382277727127075, 0.8483168482780457, 0.6108383536338806, 0.9391718506813049, 0.8798367977142334, 0.861187219619751, 0.4477795362472534, 0.8379884958267212, 0.9392204880714417, 0.9056413769721985, 0.8655614256858826, 0.5752642154693604, 0.7519539594650269, 0.39162421226501465, 0.44570887088775635, 0.2705230414867401, 0.6832618117332458, 0.795128345489502, 0.74297696352005, 0.3077659606933594, 0.5088003277778625, 0.17118844389915466, 0.362606018781662, 0.5646255612373352, 0.4412466287612915, 0.6904253959655762, 0.6649117469787598, 0.22440730035305023, 0.12013638764619827, 0.04154862463474274, 0.18146193027496338, 0.28546151518821716, 0.4314962923526764, 0.2752963602542877, 0.8828999400138855, 0.5466348528862, 0.35149943828582764, 0.3791828155517578, 0.21877993643283844, 0.3412286341190338, 0.06818217039108276, 0.19236057996749878, 0.22233277559280396, 0.6834015846252441, 0.8743255138397217, 0.936323881149292, 0.9171909093856812, 0.7217429876327515, 0.14146484434604645, 0.5480392575263977, 0.6230862140655518, 0.765608549118042, 0.9473017454147339, 0.9542620182037354, 0.994681179523468, 0.9909113049507141, 0.6502747535705566, 0.13254883885383606, 0.729562520980835, 0.8565029501914978, 0.9105069041252136, 0.9281256198883057, 0.9725569486618042, 0.9944365620613098, 0.9907113313674927, 0.8716189861297607, 0.14828605949878693, 0.9470991492271423, 0.8947573304176331, 0.4430825114250183, 0.5905793905258179, 0.889939546585083, 0.7733932733535767, 0.9838354587554932, 0.9996167421340942, 0.9999692440032959, 0.8787521719932556, 0.8215540051460266, 0.19721512496471405, 0.2126743197441101, 0.5968700051307678, 0.8293123841285706, 0.9833481311798096, 0.996432900428772, 0.9966446161270142, 0.22258009016513824, 0.34542176127433777, 0.5838942527770996, 0.29839953780174255, 0.5682299137115479, 0.8312780857086182, 0.8612236976623535, 0.870494544506073, 0.5500121116638184, 0.1392323225736618, 0.3940175175666809, 0.39858147501945496, 0.4000852108001709, 0.50374436378479, 0.5314797759056091, 0.33744892477989197, 0.39147666096687317, 0.3123686909675598, 0.01039925217628479, 0.07925508171319962, 0.5124002695083618, 0.5015945434570312, 0.3473501205444336, 0.4726306200027466, 0.4683314263820648, 0.9199329018592834, 0.8831000924110413, 0.006598725449293852, 0.05813652649521828, 0.4403068721294403, 0.4621904790401459, 0.8087036609649658, 0.7589312195777893, 0.919457197189331, 0.9823344349861145, 0.9819447994232178, 0.03517558425664902, 0.36949262022972107, 0.5504175424575806, 0.7738126516342163, 0.6672284007072449, 0.46269649267196655, 0.8927381634712219, 0.8978845477104187, 0.8883614540100098, 0.02755950577557087, 0.28665727376937866, 0.5197899341583252, 0.4367295205593109, 0.23340676724910736, 0.06667041033506393, 0.13530488312244415, 0.38548335433006287, 0.6893457174301147, 0.42430025339126587, 0.13116586208343506, 0.14841464161872864, 0.46173134446144104, 0.6305282711982727, 0.8505582809448242, 0.9533870220184326, 0.9998276829719543, 0.9999980926513672, 0.11403032392263412, 0.1292954534292221, 0.09669535607099533, 0.05391548573970795, 0.33671730756759644, 0.9419815540313721, 0.9833958745002747, 0.9950174689292908, 0.9984883069992065, 0.003536047413945198, 0.40367966890335083, 0.5019820928573608, 0.38020431995391846, 0.19449804723262787, 0.9554885625839233, 0.9396715760231018, 0.8860315680503845, 0.6959797739982605, 0.026759983971714973, 0.8654699325561523, 0.8379075527191162, 0.38650551438331604, 0.46712860465049744, 0.2914363145828247, 0.7515207529067993, 0.5643635392189026, 0.29080361127853394, 0.009798065759241581, 0.296975314617157, 0.5309689044952393, 0.9064271450042725, 0.33735230565071106, 0.2050970196723938, 0.8530158400535583, 0.9703733921051025, 0.9693025350570679, 0.0004667270986828953, 0.03012951835989952, 0.5727233290672302, 0.8237140774726868, 0.5083752274513245, 0.3019426167011261, 0.8173237442970276, 0.9788939356803894, 0.9978958964347839, 0.0005612119566649199, 0.013402573764324188, 0.4261070787906647, 0.5932590365409851, 0.14410145580768585, 0.09996624290943146, 0.7660101056098938, 0.9289748072624207, 0.9954769611358643, 5.958010660833679e-05, 0.003519759513437748, 0.006334749981760979, 0.044272616505622864, 0.05849470943212509, 0.017676575109362602, 0.07052091509103775, 0.3853133022785187, 0.7289175391197205, 0.987591564655304, 0.9621476531028748, 0.9305713176727295, 0.47957414388656616, 0.2711285352706909, 0.19621071219444275, 0.03406445309519768, 0.0042317891493439674, 0.14873303472995758, 0.5384578108787537, 0.5809404253959656, 0.6379914879798889, 0.8690799474716187, 0.5907146334648132, 0.3273884057998657, 0.03356229513883591, 0.00456766365095973, 0.016739238053560257, 0.010621413588523865, 0.17168870568275452, 0.6380153298377991, 0.8993206024169922, 0.7354894280433655, 0.5916922092437744, 0.032007016241550446, 0.01106806006282568, 0.007298346608877182, 0.4231657385826111, 0.5480770468711853, 0.7171774506568909, 0.8983364105224609, 0.28694215416908264, 0.1799968183040619, 0.06554410606622696, 0.008582701906561852, 0.015525925904512405, 0.9120689034461975, 0.2965100109577179, 0.7832893133163452, 0.8364285230636597, 0.3369685113430023, 0.1062675341963768, 0.027119474485516548, 0.04684702306985855, 0.004277986474335194, 0.4085773825645447, 0.11523397266864777, 0.07232214510440826, 0.45594727993011475, 0.8047869205474854, 0.9319777488708496, 0.8658526539802551, 0.057414814829826355, 0.0003956056898459792, 0.5265284180641174, 0.5214756727218628, 0.39651307463645935, 0.7473875880241394, 0.99171382188797, 0.9995683431625366, 0.9997905492782593, 0.32196933031082153, 0.0018490573856979609, 0.7998517155647278, 0.5577954649925232, 0.9377948641777039, 0.9877333641052246, 0.9984804391860962, 0.9999369978904724, 0.9999514818191528, 0.9636133909225464, 0.08021491020917892, 0.996428906917572, 0.9641145467758179, 0.6853483319282532, 0.6509250402450562, 0.8218783736228943, 0.7788963317871094, 0.81568443775177, 0.7887194156646729, 0.9670321941375732, 0.9613925218582153, 0.8535866141319275, 0.3318083882331848, 0.6794874668121338, 0.9070505499839783, 0.869287371635437, 0.8224495053291321, 0.9608645439147949, 0.7240593433380127, 0.23125167191028595, 0.555528998374939, 0.46142974495887756, 0.7345304489135742, 0.724733829498291, 0.783990740776062, 0.6074692010879517, 0.22682560980319977, 0.09419894218444824, 0.5021676421165466, 0.6055423021316528, 0.8101580142974854, 0.6380167007446289, 0.4412194788455963, 0.35786545276641846, 0.131900355219841, 0.12458719313144684, 0.08141861855983734, 0.2460869997739792, 0.36751994490623474, 0.5303600430488586, 0.6614041924476624, 0.7520756721496582, 0.4598398208618164, 0.26309680938720703, 0.22286592423915863, 0.24828286468982697, 0.060308534651994705, 0.0796545073390007, 0.20944902300834656, 0.7017068266868591, 0.910285472869873, 0.9710227847099304, 0.9496243596076965, 0.5010414719581604, 0.15006831288337708, 0.40715858340263367, 0.4557613432407379, 0.5323609709739685, 0.9495213627815247, 0.9690501093864441, 0.9938718676567078, 0.9716729521751404, 0.6986159086227417, 0.06323941051959991, 0.8364521861076355, 0.9264406561851501, 0.8180809020996094, 0.828368067741394, 0.9542680382728577, 0.9870510697364807, 0.9891185760498047, 0.6507614254951477, 0.4178425371646881, 0.9663060903549194, 0.8400344252586365, 0.5097991824150085, 0.5850958228111267, 0.7738450169563293, 0.8737477660179138, 0.9933934211730957, 0.9987590909004211, 0.999966561794281, 0.8023194074630737, 0.7905116677284241, 0.5517722368240356, 0.2989281415939331, 0.3933897912502289, 0.9339388012886047, 0.9911457300186157, 0.9989574551582336, 0.9971507787704468, 0.09928510338068008, 0.5666256546974182, 0.49050018191337585, 0.2756447196006775, 0.552386999130249, 0.7945618033409119, 0.8633560538291931, 0.8346832990646362, 0.5575980544090271, 0.17200976610183716, 0.6493543982505798, 0.6115214824676514, 0.35669782757759094, 0.33033955097198486, 0.399417906999588, 0.30231383442878723, 0.5617677569389343, 0.2822551727294922, 0.009622594341635704, 0.18363738059997559, 0.6594815254211426, 0.4249996840953827, 0.35886484384536743, 0.39311373233795166, 0.7519345879554749, 0.9006237983703613, 0.9768204689025879, 0.009093280881643295, 0.031184282153844833, 0.19547753036022186, 0.7578935027122498, 0.7078476548194885, 0.625069797039032, 0.9686542749404907, 0.9850186705589294, 0.9884200692176819, 0.030658746138215065, 0.30530476570129395, 0.8840422034263611, 0.8113956451416016, 0.5397434830665588, 0.32357147336006165, 0.8395619988441467, 0.9739347696304321, 0.957587480545044, 0.03256611153483391, 0.417071133852005, 0.1685718446969986, 0.32956936955451965, 0.21943964064121246, 0.07104668766260147, 0.2274424284696579, 0.3904823660850525, 0.5214771032333374, 0.41790735721588135, 0.032976970076560974, 0.07956426590681076, 0.5144582986831665, 0.6885198354721069, 0.5113798975944519, 0.9786887168884277, 0.9997603893280029, 0.9999962449073792, 0.014230278320610523, 0.153679758310318, 0.21456438302993774, 0.31134963035583496, 0.3894016146659851, 0.7964326739311218, 0.9823049306869507, 0.9968989491462708, 0.9978505969047546, 0.0048877643421292305, 0.3419152498245239, 0.1528850942850113, 0.3169884979724884, 0.3125164210796356, 0.9188878536224365, 0.9903619885444641, 0.9559500217437744, 0.5002452731132507, 0.023524479940533638, 0.6122851967811584, 0.785269558429718, 0.7433121204376221, 0.35543346405029297, 0.48104044795036316, 0.8686938285827637, 0.7973842620849609, 0.3373791575431824, 0.005650165490806103, 0.14847373962402344, 0.6271949410438538, 0.7061799764633179, 0.21048778295516968, 0.2621164619922638, 0.6940399408340454, 0.976093590259552, 0.9726244807243347, 0.00035745586501434445, 0.06120562553405762, 0.2726987600326538, 0.6937087178230286, 0.41666343808174133, 0.37002119421958923, 0.8628720641136169, 0.9960484504699707, 0.9966201186180115, 0.00043889720109291375, 0.01498471014201641, 0.581315279006958, 0.501589298248291, 0.13699813187122345, 0.06130051612854004, 0.6750178933143616, 0.9816045761108398, 0.9656457901000977, 0.00019146040722262114, 0.0022713285870850086, 0.014379934407770634, 0.023356813937425613, 0.10742271691560745, 0.045789480209350586, 0.09571020305156708, 0.5264772772789001, 0.8177812099456787, 0.9900756478309631, 0.9543907046318054, 0.8249518871307373, 0.30190685391426086, 0.39265355467796326, 0.15604837238788605, 0.04037180542945862, 0.02194703370332718, 0.10737811774015427, 0.32728466391563416, 0.3182258605957031, 0.6343241930007935, 0.5580697655677795, 0.6128929853439331, 0.6834874749183655, 0.048996880650520325, 0.05498605594038963, 0.013834970071911812, 0.038973547518253326, 0.14364953339099884, 0.5404035449028015, 0.9408915638923645, 0.7792565822601318, 0.30414214730262756, 0.06135258451104164, 0.01631486415863037, 0.010548790916800499, 0.5974174737930298, 0.7406246066093445, 0.9632142782211304, 0.8759385347366333, 0.8119038343429565, 0.1284748911857605, 0.038074616342782974, 0.007103108335286379, 0.031078804284334183, 0.8320720791816711, 0.6024851202964783, 0.6734068989753723, 0.6158350110054016, 0.26537802815437317, 0.23681573569774628, 0.049117397516965866, 0.014511737041175365, 0.0027591907419264317, 0.2608605921268463, 0.22094619274139404, 0.15447993576526642, 0.4907473623752594, 0.961796224117279, 0.826096773147583, 0.7269814014434814, 0.07466235011816025, 0.0001950188452610746, 0.5265558958053589, 0.431832492351532, 0.7248473763465881, 0.8923536539077759, 0.9960783123970032, 0.9998745918273926, 0.9993391633033752, 0.527355968952179, 0.0004658438847400248, 0.7561652064323425, 0.36604008078575134, 0.9686034321784973, 0.9929848909378052, 0.999316930770874, 0.9999777674674988, 0.9999405145645142, 0.9742678999900818, 0.05488557368516922, 0.994907021522522, 0.9440059065818787, 0.6647840142250061, 0.5374533534049988, 0.8206517696380615, 0.7992063760757446, 0.6130138635635376, 0.716776430606842, 0.9167066812515259, 0.8924567103385925, 0.8321027159690857, 0.5841452479362488, 0.618173360824585, 0.810826301574707, 0.7065442204475403, 0.8515781164169312, 0.7768171429634094, 0.6324089765548706, 0.37622448801994324, 0.6514554023742676, 0.7697067856788635, 0.48324519395828247, 0.8721766471862793, 0.7883500456809998, 0.5142235159873962, 0.3780389726161957, 0.13465914130210876, 0.5095015168190002, 0.3884643316268921, 0.5743892192840576, 0.6810908913612366, 0.5054377913475037, 0.37153539061546326, 0.05277077481150627, 0.09643670171499252, 0.11256575584411621, 0.10265465080738068, 0.21562477946281433, 0.3589496314525604, 0.8739941716194153, 0.6541025042533875, 0.38818517327308655, 0.1711486428976059, 0.22148290276527405, 0.37802624702453613, 0.06097785383462906, 0.11798015981912613, 0.2973106801509857, 0.47659656405448914, 0.8308490514755249, 0.9496622085571289, 0.8708512783050537, 0.8019254803657532, 0.14075785875320435, 0.3179510235786438, 0.5603767037391663, 0.5983523726463318, 0.9671194553375244, 0.9657195806503296, 0.9915593862533569, 0.9735866785049438, 0.5691614747047424, 0.1211380809545517, 0.8163703083992004, 0.8534916043281555, 0.8422257304191589, 0.9318249225616455, 0.9733752608299255, 0.9728540778160095, 0.9890424609184265, 0.8036941289901733, 0.2791053354740143, 0.9223984479904175, 0.8094505667686462, 0.40846577286720276, 0.4576542377471924, 0.8861232995986938, 0.8951300978660583, 0.9969214797019958, 0.9991326332092285, 0.999984622001648, 0.6298490166664124, 0.6747552752494812, 0.7512605786323547, 0.3726975619792938, 0.8901697397232056, 0.9282594919204712, 0.9853425025939941, 0.9972456097602844, 0.9981792569160461, 0.13988132774829865, 0.6795532703399658, 0.40593859553337097, 0.5593629479408264, 0.6103153228759766, 0.6124181747436523, 0.9134341478347778, 0.9111335873603821, 0.6604917645454407, 0.057073574513196945, 0.765320360660553, 0.6784078478813171, 0.7112810015678406, 0.4213520288467407, 0.3392309546470642, 0.39455342292785645, 0.5573785305023193, 0.2597452402114868, 0.01033060997724533, 0.16252169013023376, 0.3593398332595825, 0.6638529896736145, 0.553460419178009, 0.3748025596141815, 0.7075260281562805, 0.7615742087364197, 0.9123505353927612, 0.00509682297706604, 0.04891802743077278, 0.1706216037273407, 0.6480597853660583, 0.5631011128425598, 0.7187695503234863, 0.9658548831939697, 0.9881396293640137, 0.9875931143760681, 0.030774379149079323, 0.09217354655265808, 0.6552475094795227, 0.8270999193191528, 0.7596500515937805, 0.320415198802948, 0.801384449005127, 0.7498111128807068, 0.8181729912757874, 0.011919579468667507, 0.468717485666275, 0.4020167887210846, 0.23081469535827637, 0.10897964239120483, 0.067237488925457, 0.22233489155769348, 0.5190107226371765, 0.5609799027442932, 0.38197001814842224, 0.18561400473117828, 0.07153358310461044, 0.4440400302410126, 0.8608039021492004, 0.3997229039669037, 0.9816558361053467, 0.9998069405555725, 0.9999944567680359, 0.022987836971879005, 0.09487895667552948, 0.07077955454587936, 0.1640782654285431, 0.5661152601242065, 0.6670663356781006, 0.9795430898666382, 0.9984006881713867, 0.997323215007782, 0.006354407407343388, 0.4122137725353241, 0.4893490672111511, 0.43876948952674866, 0.38043081760406494, 0.7659769654273987, 0.9737034440040588, 0.8082054853439331, 0.5361558794975281, 0.012584807351231575, 0.7114486694335938, 0.7291125655174255, 0.8168485760688782, 0.555647075176239, 0.27069881558418274, 0.8506983518600464, 0.6341906785964966, 0.546419084072113, 0.01434379257261753, 0.38834530115127563, 0.663549542427063, 0.5866084098815918, 0.2492552250623703, 0.2418334186077118, 0.7181603908538818, 0.9063817262649536, 0.9860106110572815, 0.0005884304991923273, 0.020440803840756416, 0.31411197781562805, 0.8734410405158997, 0.13962341845035553, 0.24419274926185608, 0.8469001054763794, 0.9939092397689819, 0.996954083442688, 0.0007729867938905954, 0.022226175293326378, 0.36628371477127075, 0.6445885300636292, 0.3545781373977661, 0.12494275718927383, 0.3820341229438782, 0.9029123783111572, 0.9796503782272339, 2.8790118449251167e-05, 0.0075262184254825115, 0.015269679017364979, 0.03791465610265732, 0.021415431052446365, 0.017931906506419182, 0.09029135853052139, 0.4047165513038635, 0.6001816391944885] - }, - "aff_small": { - "dtype": "float32", - "shape": [3, 3, 5, 6], - "data": [0.46406716108322144, 0.5426942706108093, 0.7384114861488342, 0.5713456869125366, 2.2198104858398438, 5.418574333190918, -0.3748791217803955, 1.0331827402114868, 1.797251582145691, 0.8425048589706421, 2.0192246437072754, 3.656195640563965, -0.3702259361743927, 0.9759131669998169, 1.0483062267303467, -0.12726745009422302, 0.06490416079759598, 1.561884880065918, 1.2841030359268188, 0.29010894894599915, 0.06950980424880981, -0.903533935546875, -0.8629788756370544, 0.9620848894119263, 1.1656773090362549, 0.09103802591562271, -1.0131617784500122, -0.5291522145271301, -0.9462589621543884, 1.380102515220642, -0.13453896343708038, 0.30794498324394226, 0.009461685083806515, -0.014874253422021866, 1.2428141832351685, 4.088165283203125, -0.7917429208755493, 0.18662609159946442, 0.4709504246711731, 0.1063411608338356, 0.3878113925457001, 2.8137543201446533, -0.3206297755241394, 0.160289004445076, 0.29548266530036926, -0.5668447613716125, -0.6678410768508911, 1.4741880893707275, 0.2592640817165375, 0.13912957906723022, -0.9526811242103577, -1.7412000894546509, -0.9505654573440552, 0.7087663412094116, 1.496895432472229, 0.04148799553513527, -1.2028948068618774, -0.7139701843261719, -0.6826517581939697, 1.5241482257843018, -0.4598042964935303, -0.47573402523994446, 0.19056446850299835, -0.04603784531354904, 0.1804429143667221, 2.1676554679870605, -0.9488004446029663, -0.4213564395904541, 0.23701879382133484, -0.581139326095581, -0.3821011185646057, 1.6336822509765625, -1.6405746936798096, -0.8975228667259216, -0.4754944443702698, -1.1790424585342407, -0.8541995286941528, 0.6691067814826965, -1.152482509613037, -0.34725239872932434, -0.36326882243156433, -0.8741698265075684, -0.8011090755462646, 0.3723435699939728, 0.44391533732414246, -0.11634907126426697, -0.9914493560791016, -1.0057868957519531, -0.5518238544464111, 0.07923810184001923, -0.09202808141708374, 0.9015478491783142, 0.752293586730957, 0.347730427980423, 2.361553907394409, 5.657582759857178, -0.4648204743862152, 0.5388593673706055, 1.5677887201309204, 0.8475833535194397, 1.6359870433807373, 3.4563815593719482, 0.6727594137191772, 0.6323958039283752, 1.250576138496399, 0.07012365758419037, -0.6096585988998413, 1.9501583576202393, 0.9786921739578247, 0.2950384318828583, -0.06263409554958344, -1.2944095134735107, -1.080928087234497, 0.5081906914710999, 1.3923585414886475, -0.17504926025867462, -0.5865153670310974, -0.8047270178794861, -0.5831255316734314, 0.8187867403030396, 0.13602395355701447, -0.14631319046020508, 0.29214099049568176, -0.0764947235584259, 1.5072658061981201, 3.9891843795776367, -1.0815781354904175, 0.08645201474428177, 0.4605051875114441, -0.19348609447479248, 1.0499293804168701, 2.7861359119415283, -1.2392396926879883, 0.01796240173280239, -0.32153064012527466, -0.8047549724578857, -0.5144906044006348, 1.3717585802078247, 0.36081522703170776, -0.33160099387168884, -0.746103048324585, -1.1239019632339478, -1.1335115432739258, 0.662366509437561, 0.9657737016677856, 0.2905626595020294, -0.8116725087165833, -0.9902151823043823, -0.1980760544538498, 0.9426955580711365, -0.44737911224365234, -0.1588142365217209, 0.08121500164270401, -0.1278763860464096, 0.8424656987190247, 2.088259696960449, -0.7717563509941101, -0.8096416592597961, 0.28591227531433105, -0.5578860640525818, 0.05583227425813675, 1.4352797269821167, -2.214277982711792, -1.1825358867645264, -0.24170400202274323, -1.3039124011993408, -0.4894255995750427, 0.46510347723960876, -1.193099856376648, -0.9754549860954285, -0.30537015199661255, -1.0108152627944946, -0.8342074751853943, 0.5836665630340576, 0.05811777710914612, 0.00020262984617147595, -0.6840486526489258, -0.7087066173553467, -1.2236084938049316, -0.48058417439460754, -0.17355281114578247, 0.944849967956543, 1.0059970617294312, 0.8503601551055908, 2.443439483642578, 5.601833820343018, -0.3809331953525543, 1.0026975870132446, 1.6560070514678955, 0.749761700630188, 1.7387851476669312, 3.821709394454956, 0.37537911534309387, 1.2094063758850098, 0.6802395582199097, -0.13212935626506805, 0.1399025321006775, 1.5478019714355469, 1.170426607131958, 0.49754199385643005, 0.11478704959154129, -1.201145887374878, -0.8485324382781982, 0.8077601790428162, 1.0198428630828857, 0.1291763186454773, -1.3056211471557617, -1.2828348875045776, -0.9572649598121643, 0.789088785648346, -0.4497787058353424, 0.0951746478676796, 0.13129138946533203, -0.46670737862586975, 1.1909509897232056, 3.6424360275268555, -0.8764019012451172, -0.04159912094473839, 0.3729245960712433, -0.3381083309650421, 0.7720796465873718, 2.8585398197174072, -0.7453707456588745, -0.07765381783246994, -0.03825823590159416, -1.102720856666565, -0.02766566164791584, 1.1393375396728516, -0.003574108937755227, 0.33396902680397034, -0.35284239053726196, -1.2122776508331299, -0.6244708895683289, 1.2537063360214233, 1.5501525402069092, 0.4718341529369354, -0.6335223913192749, -0.5727731585502625, 0.07142912596464157, 1.1035889387130737, -0.43119826912879944, -0.29497578740119934, -0.01701659895479679, -0.1216130405664444, 0.6604300737380981, 2.1061322689056396, -1.2366970777511597, -0.3142439126968384, 0.5935421586036682, -0.6505181789398193, -0.1977740228176117, 1.7024857997894287, -2.1124801635742188, -0.7268149852752686, -0.922457754611969, -1.209571361541748, -0.8167011141777039, 0.4962768852710724, -1.2241511344909668, -0.9751691818237305, -0.22065073251724243, -1.377413034439087, -0.7401476502418518, 0.6528477668762207, 0.45205411314964294, -0.1473904252052307, -0.44636300206184387, -0.6158427000045776, -1.0023244619369507, -0.5611276030540466] - }, - "binary": { - "dtype": "float32", - "shape": [1, 6, 9, 10], - "data": [5.3659491539001465, 6.0133748054504395, 7.430006504058838, 8.591317176818848, 7.098993301391602, 4.9469380378723145, 0.35258030891418457, -6.256783962249756, -7.69936466217041, -5.815441131591797, 3.5772414207458496, 3.8344907760620117, 3.630661725997925, 3.7726969718933105, 2.9610977172851562, 2.0427350997924805, 0.21024785935878754, -1.6814731359481812, -0.10551862418651581, 2.0055127143859863, 2.506639242172241, 1.7424612045288086, -0.8338163495063782, -2.0285658836364746, -2.716705560684204, -2.4462459087371826, -0.11540085822343826, 1.6267147064208984, 2.0951828956604004, 2.1690876483917236, 1.7920737266540527, 1.148785948753357, -1.3262490034103394, -2.50156569480896, -3.44618821144104, -2.6358120441436768, 1.6815567016601562, 3.3389389514923096, 0.5086585283279419, -1.5670052766799927, 2.3970603942871094, 1.5622477531433105, 0.3405838906764984, -0.390401691198349, -0.8349565863609314, 0.7727837562561035, 5.153855323791504, 5.981217384338379, 2.526236057281494, 0.44679707288742065, 5.21364688873291, 3.68385648727417, 1.1133285760879517, -1.8185971975326538, -1.764566421508789, 1.800718069076538, 7.180639743804932, 8.674128532409668, 6.527144432067871, 5.504417419433594, 3.003183603286743, 2.752267837524414, 0.24273781478405, -3.9494893550872803, -4.6888108253479, -0.6619591116905212, 5.149176597595215, 7.294742107391357, 6.042765140533447, 4.368578910827637, -3.240098237991333, -2.133427619934082, -1.5094910860061646, -2.9077577590942383, -4.682331562042236, -3.728196382522583, -0.9348486065864563, -0.07644975930452347, -1.015955924987793, -3.4689276218414307, -5.786721229553223, -5.535707473754883, -3.1277503967285156, -1.0694632530212402, -1.772116780281067, -4.1227545738220215, -6.413327217102051, -8.248760223388672, -8.773528099060059, -10.277515411376953, 2.569441318511963, 4.227558612823486, 3.272855758666992, 3.2159152030944824, 3.44527006149292, 3.2036712169647217, 2.2702598571777344, -2.0255258083343506, -6.1349358558654785, -6.345519542694092, 2.960249662399292, 3.932563066482544, 1.8991639614105225, 0.6169017553329468, 0.5008314251899719, 0.9417593479156494, 1.132096529006958, -0.12411435693502426, -0.1975303441286087, 1.567750096321106, 4.545848846435547, 3.978421926498413, 0.8704116344451904, -0.847399115562439, -1.5225512981414795, -1.3644487857818604, 0.07720565795898438, 0.5476083159446716, 0.3783753216266632, 1.3271703720092773, 4.910397529602051, 4.215289115905762, 1.5627961158752441, 0.12047481536865234, -0.7083521485328674, -1.2756949663162231, 0.006725481245666742, 0.4992731809616089, -1.4448570013046265, -2.1841485500335693, 2.171921491622925, 2.992365837097168, 2.3600995540618896, 1.226008653640747, 0.9670524001121521, 0.8350412249565125, 1.4194039106369019, 1.5154738426208496, -0.06867726147174835, -0.3021414875984192, 0.6896136403083801, 2.1410462856292725, 1.9144498109817505, -0.25037315487861633, -0.14421750605106354, 1.8422211408615112, 4.0616230964660645, 4.528439998626709, 3.62046217918396, 4.437556743621826, 0.031301695853471756, 0.7169052362442017, -0.13561080396175385, -2.7123899459838867, -2.8667635917663574, 0.45719388127326965, 4.486340522766113, 5.510752201080322, 4.3953537940979, 4.193176746368408, -1.673933982849121, -2.0751988887786865, -2.228403329849243, -3.1578168869018555, -3.9310739040374756, -2.0810201168060303, 0.746999204158783, 1.20363187789917, -0.30958467721939087, -1.7441980838775635, -2.159032106399536, -3.9748692512512207, -3.4954848289489746, -2.2102558612823486, -2.783379554748535, -3.4240217208862305, -3.4968039989471436, -4.04533052444458, -5.505350589752197, -7.093589782714844, -3.873530626296997, -0.011336377821862698, 0.4875445067882538, 0.8859714269638062, 0.7773163914680481, 0.355868399143219, 1.8136941194534302, 0.882375955581665, -3.74825119972229, -6.548753261566162, -0.7724522352218628, 1.7671561241149902, 1.17444908618927, 0.06489437818527222, -0.5169049501419067, -0.05114392936229706, 0.9837242960929871, 0.52912837266922, -0.7337287664413452, -0.3258657455444336, 3.3995327949523926, 3.7973718643188477, 2.224316120147705, 0.5280616879463196, -0.4311304986476898, -0.16038045287132263, -0.007245373912155628, -0.9102384448051453, -1.360790491104126, 0.07109101861715317, 5.111376762390137, 4.7928009033203125, 2.8385355472564697, 0.8623115420341492, -0.06734935194253922, -0.9524921774864197, -1.9892375469207764, -2.1321239471435547, -2.271127223968506, -1.8610392808914185, 2.075165271759033, 3.503847122192383, 2.8292505741119385, 0.6043053865432739, -0.015673406422138214, -0.6792804598808289, -2.386014938354492, -2.2827138900756836, -1.4410916566848755, -0.4802266061306, -1.6548619270324707, 0.7469767928123474, 0.869202196598053, -1.1814311742782593, -1.0056804418563843, 0.5584226846694946, 0.6698583960533142, 0.5553838014602661, 1.2111272811889648, 2.843897819519043, -2.9796371459960938, -1.7450047731399536, -2.108497381210327, -3.1051511764526367, -2.0582103729248047, 0.836219310760498, 2.7297821044921875, 3.214021682739258, 3.12968373298645, 3.427011728286743, -1.505349040031433, -2.099647045135498, -2.934678316116333, -3.053804397583008, -2.41782283782959, -0.7890180349349976, 0.7931870818138123, 1.7800989151000977, 0.9073638319969177, -0.02210187539458275, 1.4282814264297485, -0.9839106202125549, -2.36476469039917, -2.16628098487854, -2.804795265197754, -3.5178918838500977, -2.918492555618286, -1.5013229846954346, -2.5727438926696777, -3.6148746013641357, -10.045214653015137, -4.681933879852295, -1.6427106857299805, 0.04975919425487518, 0.12663504481315613, -1.1333770751953125, -0.54898601770401, 0.36167365312576294, -0.6048591136932373, -1.199191689491272, -4.9001359939575195, -1.0641497373580933, 0.7922858595848083, 0.7870611548423767, 0.06027791276574135, 0.03230186924338341, 0.3671388626098633, 0.05977543070912361, -0.6557762026786804, 0.4021559953689575, 1.311059594154358, 2.36266827583313, 2.5424351692199707, 1.3428130149841309, 0.2641426622867584, 0.7228685617446899, 0.4575551152229309, -0.9640142321586609, -1.5785883665084839, 0.32528600096702576, 4.052007675170898, 3.4771628379821777, 1.8681128025054932, -0.358208566904068, -1.1920115947723389, -1.4710274934768677, -2.378723621368408, -2.103290557861328, -1.7309956550598145, -1.5249491930007935, 2.7331764698028564, 3.2678730487823486, 1.0367244482040405, -2.407670497894287, -2.7728323936462402, -2.6994211673736572, -4.06889009475708, -3.1028430461883545, -2.2359344959259033, -3.304476022720337, -0.280561625957489, 0.7441607117652893, -1.4427024126052856, -4.227015018463135, -3.5041961669921875, -2.1074585914611816, -2.639845848083496, -1.9072610139846802, -1.2457363605499268, -2.4698538780212402, -3.4229681491851807, -2.7518718242645264, -4.165040016174316, -4.802411079406738, -3.312793254852295, -1.8237680196762085, -1.5502851009368896, 0.14917582273483276, 1.0481963157653809, -0.6263715624809265, -2.724514961242676, -2.590906858444214, -3.501239061355591, -3.2213029861450195, -2.245868682861328, -2.445892333984375, -2.304448127746582, 0.2168741524219513, 0.5838728547096252, -1.0816328525543213, 1.6031494140625, 0.5636428594589233, -0.8043208122253418, -0.9720739722251892, -1.8723537921905518, -4.8138227462768555, -4.864011287689209, -1.7734345197677612, -1.8626002073287964, -2.492121458053589, -9.757790565490723, -6.119049549102783, -4.792038917541504, -2.3531339168548584, 0.05164617672562599, -2.159972667694092, -3.6146128177642822, -1.8688288927078247, 0.11707760393619537, 2.3063549995422363, -6.608315467834473, -3.508702516555786, -1.6583945751190186, -0.28719648718833923, 0.21881332993507385, -0.9230521321296692, -1.3013603687286377, -0.6554173231124878, -0.4879898130893707, 1.1139302253723145, -1.9055908918380737, -1.0184272527694702, 0.37065979838371277, 0.8035284280776978, -0.020805291831493378, -0.5249738097190857, 0.08855423331260681, 0.6820216774940491, 0.6364815831184387, 2.460735559463501, 0.8322417140007019, -0.021867183968424797, -0.9710642099380493, -1.8625578880310059, -2.107119083404541, -2.8084843158721924, -2.5456600189208984, -0.09064227342605591, 0.8207895159721375, 0.9201074242591858, 0.6678544878959656, 0.9817261695861816, -1.7601408958435059, -4.505643367767334, -3.732471227645874, -4.064630508422852, -4.915920734405518, -2.4779396057128906, -1.8427776098251343, -4.107543468475342, -0.10976056754589081, 0.289717435836792, -3.048628091812134, -5.741243362426758, -4.470995903015137, -3.8859617710113525, -4.1094512939453125, -2.105518341064453, -1.9972175359725952, -5.123344898223877, -2.1406466960906982, -2.4763996601104736, -4.97766637802124, -5.926474571228027, -4.454180717468262, -3.5937161445617676, -3.2313151359558105, -0.6123486757278442, -0.15748904645442963, -3.6293859481811523, -2.7107887268066406, -2.7983086109161377, -4.338146209716797, -4.532499313354492, -2.83044171333313, -3.2128748893737793, -3.921537160873413, -1.0553607940673828, -0.6731103658676147, -4.115469932556152, 0.12623330950737, 0.4490773379802704, -0.5686319470405579, -1.3069522380828857, -0.9455026388168335, -4.379739284515381, -5.920947551727295, -2.4273035526275635, -1.7730331420898438, -3.8599534034729004, -5.390627384185791, -3.5205729007720947, -5.00232458114624, -3.5243351459503174, -0.052292414009571075, -3.04063081741333, -5.477758407592773, -4.010061740875244, -2.5768470764160156, 0.025282908231019974, -6.6873393058776855, -4.9347968101501465, -4.118313789367676, -2.40698504447937, -1.095249891281128, -2.8698155879974365, -2.7994041442871094, -1.0565948486328125, -1.4256572723388672, -0.7918332815170288, -6.36838960647583, -5.763197422027588, -3.320906639099121, -1.5921087265014648, -2.06619930267334, -2.668487071990967, 0.12134291976690292, 3.5215179920196533, 3.860757827758789, 4.764191150665283, -3.825570583343506, -3.936668634414673, -3.2992238998413086, -2.8595023155212402, -2.7412831783294678, -3.645594835281372, -1.838475227355957, 3.142380714416504, 5.298643112182617, 6.142650127410889, -3.0793421268463135, -1.5801950693130493, -2.7409815788269043, -4.149155616760254, -3.3269197940826416, -4.773543834686279, -5.059864044189453, -0.9723693132400513, 0.9127416014671326, 0.3061111867427826, -2.471209764480591, -1.2496949434280396, -3.420438766479492, -5.642970085144043, -5.060687065124512, -5.00010871887207, -3.8014676570892334, -0.7229844331741333, -0.5215407609939575, -2.731050729751587, -1.7963658571243286, -2.4423987865448, -5.225739002227783, -6.9619035720825195, -5.719130992889404, -3.6851205825805664, -1.8250296115875244, 0.6239877939224243, -0.1981341391801834, -4.123570442199707, -1.7387003898620605, -2.75514817237854, -5.321733474731445, -6.459452152252197, -3.6458048820495605, -2.412551164627075, -3.5519580841064453, -1.615347981452942, -1.950024127960205, -6.693949222564697, -1.1540666818618774, -0.6208450198173523, -1.3534436225891113, -2.5150134563446045, -0.5263437628746033, -2.983214855194092, -6.41259241104126, -3.4353370666503906, -1.9550995826721191, -5.5691447257995605] - }, - "unit": { - "dtype": "float32", - "shape": [1, 5, 6, 7], - "data": [0.0011107352329418063, 0.02171722613275051, 0.6644771695137024, 0.8924037218093872, 0.868772029876709, 0.882530689239502, 0.8830705881118774, 0.009243075735867023, 0.04919278621673584, 0.41409850120544434, 0.5745526552200317, 0.7091215252876282, 0.9689579010009766, 0.9975491762161255, 0.21700987219810486, 0.2732270658016205, 0.40316805243492126, 0.3654116690158844, 0.30944469571113586, 0.7782127857208252, 0.9921198487281799, 0.7548460364341736, 0.7600014209747314, 0.7886465191841125, 0.6646921634674072, 0.33989569544792175, 0.4231291115283966, 0.8262094855308533, 0.9302034378051758, 0.9309475421905518, 0.9726762175559998, 0.9576761722564697, 0.6626627445220947, 0.3882808983325958, 0.32057657837867737, 0.9476886987686157, 0.9834905862808228, 0.998024582862854, 0.9968424439430237, 0.8939241170883179, 0.3281386196613312, 0.04673866927623749, 0.8307700157165527, 0.6565283536911011, 0.8938177227973938, 0.9711819291114807, 0.9299056529998779, 0.5232481360435486, 0.14937086403369904, 0.9220850467681885, 0.7547343969345093, 0.7710666656494141, 0.8617241382598877, 0.8895465135574341, 0.9347769618034363, 0.9727107882499695, 0.9589216709136963, 0.9200519323348999, 0.8450689315795898, 0.6854333877563477, 0.5783277750015259, 0.7983587384223938, 0.9644955992698669, 0.9096922278404236, 0.9481273293495178, 0.9619603753089905, 0.850425660610199, 0.4785035252571106, 0.36816683411598206, 0.5039082765579224, 0.7838456034660339, 0.9018192887306213, 0.9831470251083374, 0.9778647422790527, 0.7990872263908386, 0.4465848207473755, 0.27066078782081604, 0.7670353651046753, 0.9411917924880981, 0.9956544637680054, 0.9973848462104797, 0.9580102562904358, 0.6553536653518677, 0.21995459496974945, 0.9972666501998901, 0.8777324557304382, 0.9099529385566711, 0.9658845067024231, 0.6866476535797119, 0.06663477420806885, 0.035949062556028366, 0.9941718578338623, 0.8746349811553955, 0.7998111248016357, 0.8764773011207581, 0.8036499619483948, 0.730165421962738, 0.8900989890098572, 0.9712344408035278, 0.9077043533325195, 0.8098560571670532, 0.6539128422737122, 0.6793517470359802, 0.8315436840057373, 0.9290924668312073, 0.6533282399177551, 0.7746792435646057, 0.8606362342834473, 0.6761994957923889, 0.5770629048347473, 0.5932409763336182, 0.49375787377357483, 0.2666017413139343, 0.4114336669445038, 0.8811712861061096, 0.9541929960250854, 0.9318529367446899, 0.8488255739212036, 0.6221266984939575, 0.35524821281433105, 0.3988550901412964, 0.9336520433425903, 0.9961010813713074, 0.995291531085968, 0.9781524538993835, 0.8932735919952393, 0.9989046454429626, 0.9857509732246399, 0.9865124821662903, 0.9483090043067932, 0.2739749252796173, 0.05503448098897934, 0.261334091424942, 0.9903008937835693, 0.9322458505630493, 0.9431482553482056, 0.904142439365387, 0.636391282081604, 0.6370494365692139, 0.9505671858787537, 0.786812424659729, 0.7162055373191833, 0.8091251254081726, 0.7360501885414124, 0.7746759653091431, 0.896277666091919, 0.9579638242721558, 0.10965774953365326, 0.19697311520576477, 0.4303743243217468, 0.35890984535217285, 0.4945417046546936, 0.6905810236930847, 0.6093688607215881, 0.03670915588736534, 0.05892106145620346, 0.34230169653892517, 0.6755504608154297, 0.8062261343002319, 0.8655411005020142, 0.8191447257995605, 0.07763005048036575, 0.05990493297576904, 0.49162542819976807, 0.9634613990783691, 0.9880204200744629, 0.987339437007904, 0.9829182028770447, 0.9999094009399414, 0.9998578429222107, 0.9992814064025879, 0.9534087181091309, 0.3529321253299713, 0.5363808870315552, 0.9699842929840088, 0.9962427020072937, 0.9934440851211548, 0.9899517893791199, 0.9304292798042297, 0.6909584403038025, 0.8999314904212952, 0.9978291392326355, 0.5992481112480164, 0.7088947892189026, 0.8515649437904358, 0.8177461624145508, 0.9054351449012756, 0.9820400476455688, 0.997665524482727, 0.023417482152581215, 0.06901927292346954, 0.2406802624464035, 0.23283259570598602, 0.4763983190059662, 0.8505519032478333, 0.9165870547294617, 0.00622179638594389, 0.026773866266012192, 0.16463159024715424, 0.16420890390872955, 0.18173235654830933, 0.5670700073242188, 0.821071207523346, 0.012687699869275093, 0.049479395151138306, 0.32277435064315796, 0.4533427059650421, 0.4468461573123932, 0.7499678134918213, 0.9229092597961426] - }, - "signed": { - "dtype": "float32", - "shape": [2, 4, 6, 6], - "data": [-0.975315511226654, -0.9780710339546204, -0.7682645320892334, -0.10610011965036392, -0.7426806092262268, -0.9865649938583374, -0.9657320380210876, -0.9334319829940796, -0.4837842583656311, 0.11057276278734207, -0.45252591371536255, -0.8670541048049927, -0.9933829307556152, -0.9158101677894592, -0.2992991805076599, -0.17535006999969482, -0.595570981502533, -0.821949303150177, -0.9891959428787231, -0.8213410973548889, -0.539564311504364, -0.7941507697105408, -0.8773360848426819, -0.9710467457771301, -0.9325085282325745, -0.7353816628456116, -0.7151442170143127, -0.8448885083198547, -0.8760461807250977, -0.9802349805831909, -0.8781847953796387, -0.6981098055839539, -0.30355972051620483, 0.06047537550330162, -0.4124522805213928, -0.9283125400543213, -0.9105004072189331, -0.9118244647979736, -0.5608490109443665, 0.1420111358165741, -0.628115177154541, -0.9842821955680847, -0.900576651096344, -0.8706204295158386, -0.4770861864089966, 0.1892477124929428, -0.20718050003051758, -0.7667604088783264, -0.9625843167304993, -0.8790216445922852, -0.5576945543289185, -0.17448493838310242, -0.15760275721549988, -0.33947691321372986, -0.9412293434143066, -0.7334943413734436, -0.6351404190063477, -0.7322104573249817, -0.6463877558708191, -0.7571500539779663, -0.6552011966705322, -0.20973414182662964, -0.5061929225921631, -0.8417383432388306, -0.8248341083526611, -0.8894637823104858, -0.04928600788116455, 0.3999859690666199, 0.13475024700164795, -0.5339453816413879, -0.7473223209381104, -0.870064914226532, -0.6716817021369934, -0.5177398920059204, 0.47721582651138306, 0.8949826955795288, 0.6496723890304565, -0.5334205031394958, -0.25909551978111267, -0.32124313712120056, 0.26055482029914856, 0.7388795018196106, 0.5226384997367859, -0.026300983503460884, -0.4688160717487335, -0.48355841636657715, -0.2798207700252533, 0.1079116016626358, 0.08383941650390625, 0.04241274669766426, -0.8709757924079895, -0.6542706489562988, -0.4537176489830017, -0.2909751832485199, -0.0746733695268631, -0.017269672825932503, -0.8039206266403198, -0.11195512115955353, 0.07701805233955383, -0.28777649998664856, -0.1620395928621292, -0.11147403717041016, 0.17036376893520355, 0.832360029220581, 0.7889111042022705, -0.0026595823001116514, -0.3895554542541504, -0.5929332971572876, -0.7613661289215088, -0.556644082069397, 0.8309013843536377, 0.9930977821350098, 0.9886059165000916, 0.9488331079483032, 0.1194806918501854, -0.18696722388267517, 0.5470092296600342, 0.9351971745491028, 0.9218680262565613, 0.8919917345046997, -0.14818577468395233, -0.4192095100879669, -0.2821238040924072, 0.14789502322673798, 0.12116739153862, 0.38044264912605286, -0.9678853154182434, -0.862061083316803, -0.5033236742019653, -0.06847234815359116, 0.05313574895262718, 0.32553890347480774, -0.9896198511123657, -0.7896884083747864, 0.11179754137992859, 0.426102876663208, 0.4560757577419281, 0.4092296063899994, -0.7929006814956665, 0.3615199029445648, 0.8351028561592102, 0.7547246217727661, 0.47976234555244446, -0.24969157576560974, -0.7149658203125, -0.23825626075267792, 0.3540077209472656, 0.3533797562122345, -0.5333038568496704, -0.9617350697517395, 0.36302825808525085, 0.5545414686203003, 0.4973413944244385, -0.07407592982053757, -0.7189984917640686, -0.9441429376602173, 0.845985472202301, 0.8770846128463745, 0.7866882681846619, -0.1421704739332199, -0.7245355248451233, -0.7916485667228699, 0.6344623565673828, 0.823830783367157, 0.8169035911560059, -0.3459939658641815, -0.9048519730567932, -0.8990553617477417, -0.6224004626274109, -0.120573490858078, 0.24685589969158173, -0.5705562829971313, -0.9502261281013489, -0.9540281295776367, -0.9809532165527344, -0.9242095351219177, -0.5883749127388, -0.4198828339576721, -0.8282142281532288, -0.79453444480896, -0.4802651107311249, 0.1336793154478073, 0.24396173655986786, -0.4949110150337219, -0.8947134613990784, -0.9395166039466858, 0.24705049395561218, 0.5454181432723999, 0.6531095504760742, 0.05679737403988838, -0.5963600873947144, -0.7433779835700989, 0.814621090888977, 0.8810539245605469, 0.927678108215332, 0.6581257581710815, -0.028764307498931885, -0.2399357557296753, 0.7483051419258118, 0.8736684322357178, 0.9342405200004578, 0.5928245782852173, -0.4493923783302307, -0.5960139036178589, 0.15202882885932922, 0.38228318095207214, 0.6925217509269714, 0.3752192258834839, -0.5720261335372925, -0.6371177434921265, -0.5077376365661621, -0.5246010422706604, 0.12229613959789276, 0.5239750742912292, 0.08776725828647614, 0.1869756430387497, -0.30865219235420227, 0.41532132029533386, 0.5143870711326599, -0.5882659554481506, -0.9301763772964478, -0.822796106338501, -0.13912944495677948, 0.4223375618457794, 0.716825544834137, 0.06406031548976898, -0.4984789788722992, -0.2986791431903839, 0.367561012506485, 0.7353526949882507, 0.9225777387619019, 0.7424442172050476, 0.27046582102775574, 0.12740513682365417, 0.6383052468299866, 0.8470615744590759, 0.9525659680366516, 0.8005497455596924, -0.07276584208011627, -0.3639828562736511, 0.669461190700531, 0.7195366024971008, 0.9072253704071045, 0.85554438829422, 0.29120418429374695, 0.17894279956817627, 0.5325368046760559, 0.28219789266586304, 0.8068369030952454, 0.9573014378547668, 0.938581645488739, 0.9454948306083679, -0.48129191994667053, 0.46716058254241943, 0.8135022521018982, 0.1374300718307495, -0.725915253162384, -0.23464974761009216, -0.5528193116188049, 0.2926889657974243, 0.7993877530097961, 0.33474063873291016, -0.155463308095932, 0.33112379908561707, -0.22522541880607605, 0.5089237689971924, 0.8817681074142456, 0.6700969338417053, 0.30717578530311584, 0.3349846601486206, 0.6244461536407471, 0.839310348033905, 0.9506505131721497, 0.7970619201660156, -0.037546973675489426, -0.3155100345611572, 0.7073401808738708, 0.8097351789474487, 0.9560782313346863, 0.9230632185935974, 0.5844292044639587, 0.566302478313446, 0.1460425853729248, 0.3445959687232971, 0.9394413232803345, 0.9909680485725403, 0.9901589751243591, 0.992113471031189] - } - }, - "probability": [ - { - "id": "aff_unbounded", - "block": "aff_small", - "output_class": "unbounded", - "channels": [0, 1, 2], - "probability": { - "dtype": "float32", - "shape": [3, 3, 5, 6], - "data": [0.6139785647392273, 0.6324389576911926, 0.6766484379768372, 0.6390736103057861, 0.902014434337616, 0.9955860376358032, 0.40736261010169983, 0.7375324964523315, 0.8578140139579773, 0.6989925503730774, 0.882800817489624, 0.9748197793960571, 0.40848642587661743, 0.7262965440750122, 0.7404494881629944, 0.46822601556777954, 0.5162203311920166, 0.8266236186027527, 0.783147394657135, 0.5720227956771851, 0.5173704624176025, 0.2883248031139374, 0.2967173457145691, 0.7235390543937683, 0.7623627781867981, 0.522743821144104, 0.2663615345954895, 0.37071463465690613, 0.27963778376579285, 0.7990074157714844, 0.4664159417152405, 0.5763835906982422, 0.5023654103279114, 0.4962814748287201, 0.7760534882545471, 0.9835065603256226, 0.31179454922676086, 0.5465216040611267, 0.6156086921691895, 0.5265602469444275, 0.5957557559013367, 0.9434145092964172, 0.4205222427845001, 0.5399866700172424, 0.5733378529548645, 0.36196520924568176, 0.33898043632507324, 0.813693106174469, 0.5644553899765015, 0.5347263813018799, 0.2783459424972534, 0.14916056394577026, 0.2787711024284363, 0.6701284646987915, 0.8171109557151794, 0.5103704929351807, 0.2309606522321701, 0.32872217893600464, 0.33566972613334656, 0.8211485147476196, 0.38703224062919617, 0.38325998187065125, 0.5474974513053894, 0.48849260807037354, 0.5449886918067932, 0.8973070979118347, 0.2791261076927185, 0.3961922228336334, 0.5589788556098938, 0.3586704730987549, 0.4056202471256256, 0.836673378944397, 0.16238687932491302, 0.28955981135368347, 0.38331660628318787, 0.2352243959903717, 0.29855263233184814, 0.6613031625747681, 0.24003593623638153, 0.41404885053634644, 0.41016849875450134, 0.2943874001502991, 0.309788316488266, 0.5920251607894897, 0.6091915369033813, 0.4709455370903015, 0.27062591910362244, 0.26780515909194946, 0.36544138193130493, 0.5197991728782654, 0.47700923681259155, 0.7112675309181213, 0.6796782612800598, 0.5860670804977417, 0.9138482809066772, 0.9965212345123291, 0.385842889547348, 0.6315470337867737, 0.8274681568145752, 0.7000598907470703, 0.8369881510734558, 0.969420850276947, 0.6621207594871521, 0.6530324816703796, 0.7773995995521545, 0.5175237655639648, 0.35213708877563477, 0.8754639625549316, 0.7268486022949219, 0.5732291340827942, 0.4843465983867645, 0.21510738134384155, 0.25333043932914734, 0.6243822574615479, 0.8009685277938843, 0.45634907484054565, 0.35743477940559387, 0.30901527404785156, 0.35821375250816345, 0.6939787268638611, 0.5339536070823669, 0.46348682045936584, 0.5725201964378357, 0.48088565468788147, 0.8186556696891785, 0.9818218350410461, 0.2532074749469757, 0.521599531173706, 0.6131340265274048, 0.4517788589000702, 0.7407613396644592, 0.9419220685958862, 0.22456835210323334, 0.5044904947280884, 0.42030277848243713, 0.30900928378105164, 0.3741413950920105, 0.7976641654968262, 0.5892377495765686, 0.41785112023353577, 0.3216710090637207, 0.24528822302818298, 0.24351362884044647, 0.6597917675971985, 0.7242763042449951, 0.5721338987350464, 0.30753421783447266, 0.27086958289146423, 0.4506422281265259, 0.7196438312530518, 0.3899841010570526, 0.4603796899318695, 0.5202925801277161, 0.46807438135147095, 0.6989842653274536, 0.8897568583488464, 0.31609928607940674, 0.30796685814857483, 0.5709950923919678, 0.3640367388725281, 0.5139544606208801, 0.8077226281166077, 0.0984756276011467, 0.23459655046463013, 0.43986645340919495, 0.21350730955600739, 0.3800289034843445, 0.6142241358757019, 0.2327049821615219, 0.2737945318222046, 0.4242452383041382, 0.2668203115463257, 0.3027561604976654, 0.6419106721878052, 0.5145253539085388, 0.5000506639480591, 0.33535829186439514, 0.32988467812538147, 0.22730205953121185, 0.3821142017841339, 0.45672038197517395, 0.7200782895088196, 0.7322360277175903, 0.7006427049636841, 0.9200803637504578, 0.996322512626648, 0.40590184926986694, 0.73158860206604, 0.839701235294342, 0.6791267991065979, 0.8505326509475708, 0.978578507900238, 0.5927581191062927, 0.770193874835968, 0.6637921333312988, 0.46701565384864807, 0.5349187254905701, 0.8245960474014282, 0.7632221579551697, 0.6218814849853516, 0.52866530418396, 0.231271430850029, 0.29974082112312317, 0.6916319727897644, 0.7349419593811035, 0.5322492718696594, 0.2132205069065094, 0.21706806123256683, 0.2774261236190796, 0.687635600566864, 0.38941341638565063, 0.5237756967544556, 0.5327757596969604, 0.3853958547115326, 0.7669110894203186, 0.9744798541069031, 0.293923944234848, 0.48960167169570923, 0.592165470123291, 0.41626909375190735, 0.68397057056427, 0.9457584619522095, 0.3218308389186859, 0.48059627413749695, 0.4904365837574005, 0.24923042953014374, 0.49308404326438904, 0.7575579881668091, 0.4991064667701721, 0.5827248096466064, 0.4126933515071869, 0.22929827868938446, 0.3487653136253357, 0.7779408097267151, 0.8249357342720032, 0.6158177852630615, 0.34671229124069214, 0.360597163438797, 0.5178496837615967, 0.7509319186210632, 0.3938402235507965, 0.4267861545085907, 0.4957459270954132, 0.46963414549827576, 0.6593570113182068, 0.891497790813446, 0.2250114381313324, 0.4220792055130005, 0.6441774964332581, 0.3428727686405182, 0.4507170021533966, 0.8458591103553772, 0.10788971930742264, 0.3258940577507019, 0.28445735573768616, 0.22977690398693085, 0.30646437406539917, 0.6215839982032776, 0.227206751704216, 0.2738513648509979, 0.4450600743293762, 0.20142479240894318, 0.3229718506336212, 0.6576519012451172, 0.611127495765686, 0.46321895718574524, 0.3902258276939392, 0.35072755813598633, 0.2684846520423889, 0.3632865846157074] - } - }, - { - "id": "aff_one_channel", - "block": "aff_small", - "output_class": "unbounded", - "channels": [2], - "probability": { - "dtype": "float32", - "shape": [1, 3, 5, 6], - "data": [0.45672038197517395, 0.7200782895088196, 0.7322360277175903, 0.7006427049636841, 0.9200803637504578, 0.996322512626648, 0.40590184926986694, 0.73158860206604, 0.839701235294342, 0.6791267991065979, 0.8505326509475708, 0.978578507900238, 0.5927581191062927, 0.770193874835968, 0.6637921333312988, 0.46701565384864807, 0.5349187254905701, 0.8245960474014282, 0.7632221579551697, 0.6218814849853516, 0.52866530418396, 0.231271430850029, 0.29974082112312317, 0.6916319727897644, 0.7349419593811035, 0.5322492718696594, 0.2132205069065094, 0.21706806123256683, 0.2774261236190796, 0.687635600566864, 0.38941341638565063, 0.5237756967544556, 0.5327757596969604, 0.3853958547115326, 0.7669110894203186, 0.9744798541069031, 0.293923944234848, 0.48960167169570923, 0.592165470123291, 0.41626909375190735, 0.68397057056427, 0.9457584619522095, 0.3218308389186859, 0.48059627413749695, 0.4904365837574005, 0.24923042953014374, 0.49308404326438904, 0.7575579881668091, 0.4991064667701721, 0.5827248096466064, 0.4126933515071869, 0.22929827868938446, 0.3487653136253357, 0.7779408097267151, 0.8249357342720032, 0.6158177852630615, 0.34671229124069214, 0.360597163438797, 0.5178496837615967, 0.7509319186210632, 0.3938402235507965, 0.4267861545085907, 0.4957459270954132, 0.46963414549827576, 0.6593570113182068, 0.891497790813446, 0.2250114381313324, 0.4220792055130005, 0.6441774964332581, 0.3428727686405182, 0.4507170021533966, 0.8458591103553772, 0.10788971930742264, 0.3258940577507019, 0.28445735573768616, 0.22977690398693085, 0.30646437406539917, 0.6215839982032776, 0.227206751704216, 0.2738513648509979, 0.4450600743293762, 0.20142479240894318, 0.3229718506336212, 0.6576519012451172, 0.611127495765686, 0.46321895718574524, 0.3902258276939392, 0.35072755813598633, 0.2684846520423889, 0.3632865846157074] - } - }, - { - "id": "aff_classified", - "block": "aff_small", - "output_class": null, - "channels": null, - "probability": { - "dtype": "float32", - "shape": [3, 3, 5, 6], - "data": [0.6139785647392273, 0.6324389576911926, 0.6766484379768372, 0.6390736103057861, 0.902014434337616, 0.9955860376358032, 0.40736261010169983, 0.7375324964523315, 0.8578140139579773, 0.6989925503730774, 0.882800817489624, 0.9748197793960571, 0.40848642587661743, 0.7262965440750122, 0.7404494881629944, 0.46822601556777954, 0.5162203311920166, 0.8266236186027527, 0.783147394657135, 0.5720227956771851, 0.5173704624176025, 0.2883248031139374, 0.2967173457145691, 0.7235390543937683, 0.7623627781867981, 0.522743821144104, 0.2663615345954895, 0.37071463465690613, 0.27963778376579285, 0.7990074157714844, 0.4664159417152405, 0.5763835906982422, 0.5023654103279114, 0.4962814748287201, 0.7760534882545471, 0.9835065603256226, 0.31179454922676086, 0.5465216040611267, 0.6156086921691895, 0.5265602469444275, 0.5957557559013367, 0.9434145092964172, 0.4205222427845001, 0.5399866700172424, 0.5733378529548645, 0.36196520924568176, 0.33898043632507324, 0.813693106174469, 0.5644553899765015, 0.5347263813018799, 0.2783459424972534, 0.14916056394577026, 0.2787711024284363, 0.6701284646987915, 0.8171109557151794, 0.5103704929351807, 0.2309606522321701, 0.32872217893600464, 0.33566972613334656, 0.8211485147476196, 0.38703224062919617, 0.38325998187065125, 0.5474974513053894, 0.48849260807037354, 0.5449886918067932, 0.8973070979118347, 0.2791261076927185, 0.3961922228336334, 0.5589788556098938, 0.3586704730987549, 0.4056202471256256, 0.836673378944397, 0.16238687932491302, 0.28955981135368347, 0.38331660628318787, 0.2352243959903717, 0.29855263233184814, 0.6613031625747681, 0.24003593623638153, 0.41404885053634644, 0.41016849875450134, 0.2943874001502991, 0.309788316488266, 0.5920251607894897, 0.6091915369033813, 0.4709455370903015, 0.27062591910362244, 0.26780515909194946, 0.36544138193130493, 0.5197991728782654, 0.47700923681259155, 0.7112675309181213, 0.6796782612800598, 0.5860670804977417, 0.9138482809066772, 0.9965212345123291, 0.385842889547348, 0.6315470337867737, 0.8274681568145752, 0.7000598907470703, 0.8369881510734558, 0.969420850276947, 0.6621207594871521, 0.6530324816703796, 0.7773995995521545, 0.5175237655639648, 0.35213708877563477, 0.8754639625549316, 0.7268486022949219, 0.5732291340827942, 0.4843465983867645, 0.21510738134384155, 0.25333043932914734, 0.6243822574615479, 0.8009685277938843, 0.45634907484054565, 0.35743477940559387, 0.30901527404785156, 0.35821375250816345, 0.6939787268638611, 0.5339536070823669, 0.46348682045936584, 0.5725201964378357, 0.48088565468788147, 0.8186556696891785, 0.9818218350410461, 0.2532074749469757, 0.521599531173706, 0.6131340265274048, 0.4517788589000702, 0.7407613396644592, 0.9419220685958862, 0.22456835210323334, 0.5044904947280884, 0.42030277848243713, 0.30900928378105164, 0.3741413950920105, 0.7976641654968262, 0.5892377495765686, 0.41785112023353577, 0.3216710090637207, 0.24528822302818298, 0.24351362884044647, 0.6597917675971985, 0.7242763042449951, 0.5721338987350464, 0.30753421783447266, 0.27086958289146423, 0.4506422281265259, 0.7196438312530518, 0.3899841010570526, 0.4603796899318695, 0.5202925801277161, 0.46807438135147095, 0.6989842653274536, 0.8897568583488464, 0.31609928607940674, 0.30796685814857483, 0.5709950923919678, 0.3640367388725281, 0.5139544606208801, 0.8077226281166077, 0.0984756276011467, 0.23459655046463013, 0.43986645340919495, 0.21350730955600739, 0.3800289034843445, 0.6142241358757019, 0.2327049821615219, 0.2737945318222046, 0.4242452383041382, 0.2668203115463257, 0.3027561604976654, 0.6419106721878052, 0.5145253539085388, 0.5000506639480591, 0.33535829186439514, 0.32988467812538147, 0.22730205953121185, 0.3821142017841339, 0.45672038197517395, 0.7200782895088196, 0.7322360277175903, 0.7006427049636841, 0.9200803637504578, 0.996322512626648, 0.40590184926986694, 0.73158860206604, 0.839701235294342, 0.6791267991065979, 0.8505326509475708, 0.978578507900238, 0.5927581191062927, 0.770193874835968, 0.6637921333312988, 0.46701565384864807, 0.5349187254905701, 0.8245960474014282, 0.7632221579551697, 0.6218814849853516, 0.52866530418396, 0.231271430850029, 0.29974082112312317, 0.6916319727897644, 0.7349419593811035, 0.5322492718696594, 0.2132205069065094, 0.21706806123256683, 0.2774261236190796, 0.687635600566864, 0.38941341638565063, 0.5237756967544556, 0.5327757596969604, 0.3853958547115326, 0.7669110894203186, 0.9744798541069031, 0.293923944234848, 0.48960167169570923, 0.592165470123291, 0.41626909375190735, 0.68397057056427, 0.9457584619522095, 0.3218308389186859, 0.48059627413749695, 0.4904365837574005, 0.24923042953014374, 0.49308404326438904, 0.7575579881668091, 0.4991064667701721, 0.5827248096466064, 0.4126933515071869, 0.22929827868938446, 0.3487653136253357, 0.7779408097267151, 0.8249357342720032, 0.6158177852630615, 0.34671229124069214, 0.360597163438797, 0.5178496837615967, 0.7509319186210632, 0.3938402235507965, 0.4267861545085907, 0.4957459270954132, 0.46963414549827576, 0.6593570113182068, 0.891497790813446, 0.2250114381313324, 0.4220792055130005, 0.6441774964332581, 0.3428727686405182, 0.4507170021533966, 0.8458591103553772, 0.10788971930742264, 0.3258940577507019, 0.28445735573768616, 0.22977690398693085, 0.30646437406539917, 0.6215839982032776, 0.227206751704216, 0.2738513648509979, 0.4450600743293762, 0.20142479240894318, 0.3229718506336212, 0.6576519012451172, 0.611127495765686, 0.46321895718574524, 0.3902258276939392, 0.35072755813598633, 0.2684846520423889, 0.3632865846157074] - } - }, - { - "id": "binary_unbounded", - "block": "binary", - "output_class": "unbounded", - "channels": null, - "probability": { - "dtype": "float32", - "shape": [1, 6, 9, 10], - "data": [0.9953487515449524, 0.9975601434707642, 0.999407172203064, 0.9998143315315247, 0.9991747736930847, 0.9929450154304504, 0.5872431993484497, 0.001913732965476811, 0.0004529097641352564, 0.0029723041225224733, 0.9728074073791504, 0.978844940662384, 0.9741854071617126, 0.9775266647338867, 0.9507853388786316, 0.8852114677429199, 0.552369236946106, 0.15690049529075623, 0.47364482283592224, 0.881374716758728, 0.9246059656143188, 0.8509994745254517, 0.3028387427330017, 0.11623616516590118, 0.0619947649538517, 0.07971350848674774, 0.4711817502975464, 0.835719108581543, 0.8904341459274292, 0.8974390029907227, 0.8571813106536865, 0.7592891454696655, 0.2097804993391037, 0.0757484957575798, 0.030882740393280983, 0.06686887890100479, 0.8431105613708496, 0.9657407999038696, 0.6244919300079346, 0.1726437360048294, 0.9166028499603271, 0.8266756534576416, 0.5843323469161987, 0.40362057089805603, 0.3025980591773987, 0.6841227412223816, 0.9942560791969299, 0.997480571269989, 0.9259607195854187, 0.6098774075508118, 0.9945876598358154, 0.9754899740219116, 0.7527491450309753, 0.13960228860378265, 0.14621934294700623, 0.8582363724708557, 0.999239444732666, 0.9998290538787842, 0.9985389709472656, 0.9959477782249451, 0.9527177214622498, 0.9400413036346436, 0.5603882074356079, 0.0189004298299551, 0.009113792330026627, 0.34029966592788696, 0.9942293167114258, 0.9993213415145874, 0.9976305961608887, 0.9874892830848694, 0.037684328854084015, 0.10589003562927246, 0.18101422488689423, 0.05177140235900879, 0.009172490797936916, 0.023471971973776817, 0.28194206953048706, 0.48089686036109924, 0.2658158838748932, 0.030209382995963097, 0.003058641217648983, 0.0039279283955693245, 0.04197698086500168, 0.25550517439842224, 0.14527928829193115, 0.015941578894853592, 0.0016368766082450747, 0.00026151424390263855, 0.00015475258987862617, 3.4396703995298594e-05, 0.9288687109947205, 0.9856217503547668, 0.9634857773780823, 0.9614288210868835, 0.9690898060798645, 0.9609721899032593, 0.9063838720321655, 0.1165488213300705, 0.0021611833944916725, 0.001751517760567367, 0.950745701789856, 0.9807831645011902, 0.869796872138977, 0.6495135426521301, 0.6226546764373779, 0.7194548845291138, 0.7562255859375, 0.4690111577510834, 0.4507773816585541, 0.8274626731872559, 0.9895002245903015, 0.9816286563873291, 0.7048313617706299, 0.29997873306274414, 0.1790861338376999, 0.20351821184158325, 0.5192918181419373, 0.6335805058479309, 0.593481183052063, 0.7903721928596497, 0.9926844239234924, 0.9854469299316406, 0.8267542123794556, 0.5300823450088501, 0.3299630582332611, 0.21828393638134003, 0.501681387424469, 0.6222884654998779, 0.19079433381557465, 0.10118301957845688, 0.8976995348930359, 0.9522280097007751, 0.913733720779419, 0.7731192111968994, 0.7245315909385681, 0.6974198222160339, 0.8052449822425842, 0.8198710083961487, 0.482837438583374, 0.4250340461730957, 0.6658809781074524, 0.8948291540145874, 0.8715182542800903, 0.43773165345191956, 0.46400803327560425, 0.8632112145423889, 0.983070433139801, 0.9893178343772888, 0.9739276170730591, 0.9883133172988892, 0.5078247785568237, 0.6719251871109009, 0.4661491811275482, 0.06224619597196579, 0.053821224719285965, 0.6123482584953308, 0.9888635873794556, 0.9959731698036194, 0.987815797328949, 0.9851263761520386, 0.1579003781080246, 0.11153082549571991, 0.09722869843244553, 0.04078437387943268, 0.019244952127337456, 0.11095529049634933, 0.6785244941711426, 0.7691702842712402, 0.4232161045074463, 0.1487804800271988, 0.10349021852016449, 0.018435506150126457, 0.029440974816679955, 0.09883328527212143, 0.05822895094752312, 0.031553104519844055, 0.02940330281853676, 0.017202802002429962, 0.004048506263643503, 0.0008297220338135958, 0.0203616414219141, 0.4971659779548645, 0.6195278167724609, 0.7080581188201904, 0.6851014494895935, 0.5880399346351624, 0.8598077893257141, 0.7073143124580383, 0.023016663268208504, 0.001429852331057191, 0.31594887375831604, 0.8541036248207092, 0.7639482617378235, 0.5162178874015808, 0.37357622385025024, 0.4872167706489563, 0.7278465628623962, 0.6292797923088074, 0.32437700033187866, 0.4192468822002411, 0.9676899313926697, 0.9780624508857727, 0.9024119973182678, 0.6290309429168701, 0.3938564360141754, 0.45999062061309814, 0.49818864464759827, 0.2869510352611542, 0.20411185920238495, 0.5177652835845947, 0.9940083026885986, 0.9917789101600647, 0.9447230696678162, 0.7031433582305908, 0.48316898941993713, 0.27838391065597534, 0.12033756077289581, 0.10601352900266647, 0.09354258328676224, 0.13458196818828583, 0.8884658217430115, 0.970797061920166, 0.944236159324646, 0.6466406583786011, 0.49608170986175537, 0.336421936750412, 0.08424536138772964, 0.09256473928689957, 0.19137634336948395, 0.38219860196113586, 0.1604529321193695, 0.6785195469856262, 0.7045796513557434, 0.2347949594259262, 0.26782605051994324, 0.6360874772071838, 0.6614714860916138, 0.6353837251663208, 0.7704983353614807, 0.9450024366378784, 0.04835432395339012, 0.14867834746837616, 0.10827365517616272, 0.04289527237415314, 0.11322538554668427, 0.6976683735847473, 0.9387613534927368, 0.9613585472106934, 0.9581007361412048, 0.9685381650924683, 0.18162909150123596, 0.10913113504648209, 0.05046567693352699, 0.04505350813269615, 0.0818236693739891, 0.31237953901290894, 0.6885152459144592, 0.8557091355323792, 0.7124603986740112, 0.49447476863861084, 0.8066333532333374, 0.2721165120601654, 0.08589932322502136, 0.10281959921121597, 0.05706559494137764, 0.028807416558265686, 0.051246944814920425, 0.18222828209400177, 0.07091331481933594, 0.0262145958840847, 4.339102088124491e-05, 0.009176106192171574, 0.1620965451002121, 0.5124372243881226, 0.5316165089607239, 0.24353839457035065, 0.3660997152328491, 0.5894455313682556, 0.35323280096054077, 0.23161904513835907, 0.007390543818473816, 0.256517231464386, 0.6883218884468079, 0.6871999502182007, 0.5150649547576904, 0.5080747604370117, 0.5907674431800842, 0.5149394273757935, 0.3416890501976013, 0.5992055535316467, 0.7876904010772705, 0.9139358997344971, 0.9270637035369873, 0.7929521799087524, 0.5656543970108032, 0.6732383966445923, 0.6124340295791626, 0.27607521414756775, 0.17099548876285553, 0.5806119441986084, 0.9829097390174866, 0.9700309038162231, 0.8662397861480713, 0.41139328479766846, 0.23289935290813446, 0.186786487698555, 0.08480957895517349, 0.10877740383148193, 0.15046027302742004, 0.17873388528823853, 0.9389561414718628, 0.9633100628852844, 0.7382174730300903, 0.08258964866399765, 0.058810044080019, 0.06300751864910126, 0.01680898107588291, 0.04299013689160347, 0.09656965732574463, 0.03541795536875725, 0.43031612038612366, 0.6779050230979919, 0.191127210855484, 0.014385918155312538, 0.029193071648478508, 0.10837399959564209, 0.06661762297153473, 0.12928886711597443, 0.2234390676021576, 0.07799874246120453, 0.03158531337976456, 0.05998102203011513, 0.015291629359126091, 0.008143074810504913, 0.03513490408658981, 0.138982355594635, 0.17504508793354034, 0.5372249484062195, 0.7404283881187439, 0.3483337461948395, 0.06154219061136246, 0.06972593814134598, 0.029276996850967407, 0.03837187588214874, 0.09570641815662384, 0.07973945885896683, 0.09075523167848587, 0.5540069937705994, 0.6419580578804016, 0.2531971335411072, 0.8324581384658813, 0.6372949481010437, 0.30910199880599976, 0.2744673192501068, 0.13326960802078247, 0.00805142056196928, 0.007660322822630405, 0.14511573314666748, 0.13440026342868805, 0.07641234248876572, 5.783893357147463e-05, 0.0021957147400826216, 0.008227276615798473, 0.08681699633598328, 0.5129086971282959, 0.1034029871225357, 0.02622128091752529, 0.13367728888988495, 0.5292360186576843, 0.9094020128250122, 0.0013472852297127247, 0.029065631330013275, 0.15997762978076935, 0.4286903738975525, 0.554486095905304, 0.2843363881111145, 0.21393616497516632, 0.3417697846889496, 0.3803672194480896, 0.7528610825538635, 0.12947700917720795, 0.2653338611125946, 0.5916183590888977, 0.6907287836074829, 0.494798868894577, 0.37168994545936584, 0.5221241116523743, 0.6641897559165955, 0.6539576649665833, 0.9213430285453796, 0.6968287229537964, 0.4945334196090698, 0.274668425321579, 0.1344051957130432, 0.10840680450201035, 0.05686741694808006, 0.07271859049797058, 0.4773549437522888, 0.694403886795044, 0.7150639891624451, 0.6610226035118103, 0.7274506092071533, 0.14677269756793976, 0.01092578936368227, 0.02337418869137764, 0.016879521310329437, 0.0072756437584757805, 0.0774192363023758, 0.1367231160402298, 0.01618196628987789, 0.4725874066352844, 0.5719269514083862, 0.045276742428541183, 0.003200497478246689, 0.01130661927163601, 0.02011515013873577, 0.01615162193775177, 0.10856162756681442, 0.11949537694454193, 0.005920802243053913, 0.10520849376916885, 0.07752930372953415, 0.00684297364205122, 0.0026607720647007227, 0.011496146209537983, 0.02676016464829445, 0.03800413757562637, 0.35152363777160645, 0.4607089161872864, 0.0258466936647892, 0.06233973428606987, 0.057415641844272614, 0.012892334721982479, 0.010639351792633533, 0.055701158940792084, 0.03868408128619194, 0.019425783306360245, 0.25819700956344604, 0.3378007113933563, 0.016056260094046593, 0.5315164923667908, 0.6104198694229126, 0.36155256628990173, 0.21299730241298676, 0.27979016304016113, 0.012373600155115128, 0.0026754795107990503, 0.08111421763896942, 0.14516553282737732, 0.02063423953950405, 0.004538421053439379, 0.028732504695653915, 0.006677414756268263, 0.028627697378396988, 0.48692989349365234, 0.045623697340488434, 0.004161297343671322, 0.017809351906180382, 0.07064344733953476, 0.5063204169273376, 0.0012450431240722537, 0.007140567526221275, 0.016011392697691917, 0.0826416015625, 0.2506309747695923, 0.053666017949581146, 0.05735638365149498, 0.257960706949234, 0.1937762349843979, 0.3117751479148865, 0.0017119827680289745, 0.0031312168575823307, 0.03486089035868645, 0.16908740997314453, 0.11242574453353882, 0.06485866755247116, 0.5302985906600952, 0.9712939262390137, 0.9793820381164551, 0.9915423393249512, 0.021340636536478996, 0.019139638170599937, 0.035597823560237885, 0.054192204028367996, 0.06058083474636078, 0.025441696867346764, 0.1372317224740982, 0.9586074352264404, 0.9950265288352966, 0.9978553652763367, 0.043967459350824356, 0.1707678586244583, 0.06059800460934639, 0.0155326621606946, 0.034659139811992645, 0.008379570208489895, 0.006306399125605822, 0.2744084894657135, 0.7135608792304993, 0.5759357810020447, 0.07790128886699677, 0.22275295853614807, 0.031662773340940475, 0.003529828041791916, 0.006301243789494038, 0.0066921282559633255, 0.02184988185763359, 0.3267361521720886, 0.3724920153617859, 0.06116579473018646, 0.1422940343618393, 0.07999619096517563, 0.005347634665668011, 0.0009463950991630554, 0.003271822351962328, 0.024479849264025688, 0.13883145153522491, 0.6511249542236328, 0.4506279230117798, 0.015928784385323524, 0.14947809278964996, 0.05979656055569649, 0.0048605394549667835, 0.0015632057329639792, 0.025436490774154663, 0.08222059905529022, 0.02786947600543499, 0.16584743559360504, 0.1245507299900055, 0.0012368507450446486, 0.2397470623254776, 0.3495893180370331, 0.2053079605102539, 0.07481236010789871, 0.3713700771331787, 0.048189956694841385, 0.001638077781535685, 0.03120916150510311, 0.12399835884571075, 0.0037992512807250023] - } - }, - { - "id": "unit_unit", - "block": "unit", - "output_class": "unit", - "channels": null, - "probability": { - "dtype": "float32", - "shape": [1, 5, 6, 7], - "data": [0.0011107352329418063, 0.02171722613275051, 0.6644771695137024, 0.8924037218093872, 0.868772029876709, 0.882530689239502, 0.8830705881118774, 0.009243075735867023, 0.04919278621673584, 0.41409850120544434, 0.5745526552200317, 0.7091215252876282, 0.9689579010009766, 0.9975491762161255, 0.21700987219810486, 0.2732270658016205, 0.40316805243492126, 0.3654116690158844, 0.30944469571113586, 0.7782127857208252, 0.9921198487281799, 0.7548460364341736, 0.7600014209747314, 0.7886465191841125, 0.6646921634674072, 0.33989569544792175, 0.4231291115283966, 0.8262094855308533, 0.9302034378051758, 0.9309475421905518, 0.9726762175559998, 0.9576761722564697, 0.6626627445220947, 0.3882808983325958, 0.32057657837867737, 0.9476886987686157, 0.9834905862808228, 0.998024582862854, 0.9968424439430237, 0.8939241170883179, 0.3281386196613312, 0.04673866927623749, 0.8307700157165527, 0.6565283536911011, 0.8938177227973938, 0.9711819291114807, 0.9299056529998779, 0.5232481360435486, 0.14937086403369904, 0.9220850467681885, 0.7547343969345093, 0.7710666656494141, 0.8617241382598877, 0.8895465135574341, 0.9347769618034363, 0.9727107882499695, 0.9589216709136963, 0.9200519323348999, 0.8450689315795898, 0.6854333877563477, 0.5783277750015259, 0.7983587384223938, 0.9644955992698669, 0.9096922278404236, 0.9481273293495178, 0.9619603753089905, 0.850425660610199, 0.4785035252571106, 0.36816683411598206, 0.5039082765579224, 0.7838456034660339, 0.9018192887306213, 0.9831470251083374, 0.9778647422790527, 0.7990872263908386, 0.4465848207473755, 0.27066078782081604, 0.7670353651046753, 0.9411917924880981, 0.9956544637680054, 0.9973848462104797, 0.9580102562904358, 0.6553536653518677, 0.21995459496974945, 0.9972666501998901, 0.8777324557304382, 0.9099529385566711, 0.9658845067024231, 0.6866476535797119, 0.06663477420806885, 0.035949062556028366, 0.9941718578338623, 0.8746349811553955, 0.7998111248016357, 0.8764773011207581, 0.8036499619483948, 0.730165421962738, 0.8900989890098572, 0.9712344408035278, 0.9077043533325195, 0.8098560571670532, 0.6539128422737122, 0.6793517470359802, 0.8315436840057373, 0.9290924668312073, 0.6533282399177551, 0.7746792435646057, 0.8606362342834473, 0.6761994957923889, 0.5770629048347473, 0.5932409763336182, 0.49375787377357483, 0.2666017413139343, 0.4114336669445038, 0.8811712861061096, 0.9541929960250854, 0.9318529367446899, 0.8488255739212036, 0.6221266984939575, 0.35524821281433105, 0.3988550901412964, 0.9336520433425903, 0.9961010813713074, 0.995291531085968, 0.9781524538993835, 0.8932735919952393, 0.9989046454429626, 0.9857509732246399, 0.9865124821662903, 0.9483090043067932, 0.2739749252796173, 0.05503448098897934, 0.261334091424942, 0.9903008937835693, 0.9322458505630493, 0.9431482553482056, 0.904142439365387, 0.636391282081604, 0.6370494365692139, 0.9505671858787537, 0.786812424659729, 0.7162055373191833, 0.8091251254081726, 0.7360501885414124, 0.7746759653091431, 0.896277666091919, 0.9579638242721558, 0.10965774953365326, 0.19697311520576477, 0.4303743243217468, 0.35890984535217285, 0.4945417046546936, 0.6905810236930847, 0.6093688607215881, 0.03670915588736534, 0.05892106145620346, 0.34230169653892517, 0.6755504608154297, 0.8062261343002319, 0.8655411005020142, 0.8191447257995605, 0.07763005048036575, 0.05990493297576904, 0.49162542819976807, 0.9634613990783691, 0.9880204200744629, 0.987339437007904, 0.9829182028770447, 0.9999094009399414, 0.9998578429222107, 0.9992814064025879, 0.9534087181091309, 0.3529321253299713, 0.5363808870315552, 0.9699842929840088, 0.9962427020072937, 0.9934440851211548, 0.9899517893791199, 0.9304292798042297, 0.6909584403038025, 0.8999314904212952, 0.9978291392326355, 0.5992481112480164, 0.7088947892189026, 0.8515649437904358, 0.8177461624145508, 0.9054351449012756, 0.9820400476455688, 0.997665524482727, 0.023417482152581215, 0.06901927292346954, 0.2406802624464035, 0.23283259570598602, 0.4763983190059662, 0.8505519032478333, 0.9165870547294617, 0.00622179638594389, 0.026773866266012192, 0.16463159024715424, 0.16420890390872955, 0.18173235654830933, 0.5670700073242188, 0.821071207523346, 0.012687699869275093, 0.049479395151138306, 0.32277435064315796, 0.4533427059650421, 0.4468461573123932, 0.7499678134918213, 0.9229092597961426] - } - }, - { - "id": "unit_classified", - "block": "unit", - "output_class": null, - "channels": null, - "probability": { - "dtype": "float32", - "shape": [1, 5, 6, 7], - "data": [0.0011107352329418063, 0.02171722613275051, 0.6644771695137024, 0.8924037218093872, 0.868772029876709, 0.882530689239502, 0.8830705881118774, 0.009243075735867023, 0.04919278621673584, 0.41409850120544434, 0.5745526552200317, 0.7091215252876282, 0.9689579010009766, 0.9975491762161255, 0.21700987219810486, 0.2732270658016205, 0.40316805243492126, 0.3654116690158844, 0.30944469571113586, 0.7782127857208252, 0.9921198487281799, 0.7548460364341736, 0.7600014209747314, 0.7886465191841125, 0.6646921634674072, 0.33989569544792175, 0.4231291115283966, 0.8262094855308533, 0.9302034378051758, 0.9309475421905518, 0.9726762175559998, 0.9576761722564697, 0.6626627445220947, 0.3882808983325958, 0.32057657837867737, 0.9476886987686157, 0.9834905862808228, 0.998024582862854, 0.9968424439430237, 0.8939241170883179, 0.3281386196613312, 0.04673866927623749, 0.8307700157165527, 0.6565283536911011, 0.8938177227973938, 0.9711819291114807, 0.9299056529998779, 0.5232481360435486, 0.14937086403369904, 0.9220850467681885, 0.7547343969345093, 0.7710666656494141, 0.8617241382598877, 0.8895465135574341, 0.9347769618034363, 0.9727107882499695, 0.9589216709136963, 0.9200519323348999, 0.8450689315795898, 0.6854333877563477, 0.5783277750015259, 0.7983587384223938, 0.9644955992698669, 0.9096922278404236, 0.9481273293495178, 0.9619603753089905, 0.850425660610199, 0.4785035252571106, 0.36816683411598206, 0.5039082765579224, 0.7838456034660339, 0.9018192887306213, 0.9831470251083374, 0.9778647422790527, 0.7990872263908386, 0.4465848207473755, 0.27066078782081604, 0.7670353651046753, 0.9411917924880981, 0.9956544637680054, 0.9973848462104797, 0.9580102562904358, 0.6553536653518677, 0.21995459496974945, 0.9972666501998901, 0.8777324557304382, 0.9099529385566711, 0.9658845067024231, 0.6866476535797119, 0.06663477420806885, 0.035949062556028366, 0.9941718578338623, 0.8746349811553955, 0.7998111248016357, 0.8764773011207581, 0.8036499619483948, 0.730165421962738, 0.8900989890098572, 0.9712344408035278, 0.9077043533325195, 0.8098560571670532, 0.6539128422737122, 0.6793517470359802, 0.8315436840057373, 0.9290924668312073, 0.6533282399177551, 0.7746792435646057, 0.8606362342834473, 0.6761994957923889, 0.5770629048347473, 0.5932409763336182, 0.49375787377357483, 0.2666017413139343, 0.4114336669445038, 0.8811712861061096, 0.9541929960250854, 0.9318529367446899, 0.8488255739212036, 0.6221266984939575, 0.35524821281433105, 0.3988550901412964, 0.9336520433425903, 0.9961010813713074, 0.995291531085968, 0.9781524538993835, 0.8932735919952393, 0.9989046454429626, 0.9857509732246399, 0.9865124821662903, 0.9483090043067932, 0.2739749252796173, 0.05503448098897934, 0.261334091424942, 0.9903008937835693, 0.9322458505630493, 0.9431482553482056, 0.904142439365387, 0.636391282081604, 0.6370494365692139, 0.9505671858787537, 0.786812424659729, 0.7162055373191833, 0.8091251254081726, 0.7360501885414124, 0.7746759653091431, 0.896277666091919, 0.9579638242721558, 0.10965774953365326, 0.19697311520576477, 0.4303743243217468, 0.35890984535217285, 0.4945417046546936, 0.6905810236930847, 0.6093688607215881, 0.03670915588736534, 0.05892106145620346, 0.34230169653892517, 0.6755504608154297, 0.8062261343002319, 0.8655411005020142, 0.8191447257995605, 0.07763005048036575, 0.05990493297576904, 0.49162542819976807, 0.9634613990783691, 0.9880204200744629, 0.987339437007904, 0.9829182028770447, 0.9999094009399414, 0.9998578429222107, 0.9992814064025879, 0.9534087181091309, 0.3529321253299713, 0.5363808870315552, 0.9699842929840088, 0.9962427020072937, 0.9934440851211548, 0.9899517893791199, 0.9304292798042297, 0.6909584403038025, 0.8999314904212952, 0.9978291392326355, 0.5992481112480164, 0.7088947892189026, 0.8515649437904358, 0.8177461624145508, 0.9054351449012756, 0.9820400476455688, 0.997665524482727, 0.023417482152581215, 0.06901927292346954, 0.2406802624464035, 0.23283259570598602, 0.4763983190059662, 0.8505519032478333, 0.9165870547294617, 0.00622179638594389, 0.026773866266012192, 0.16463159024715424, 0.16420890390872955, 0.18173235654830933, 0.5670700073242188, 0.821071207523346, 0.012687699869275093, 0.049479395151138306, 0.32277435064315796, 0.4533427059650421, 0.4468461573123932, 0.7499678134918213, 0.9229092597961426] - } - }, - { - "id": "signed_signed", - "block": "signed", - "output_class": "signed_unit", - "channels": null, - "probability": { - "dtype": "float32", - "shape": [2, 4, 6, 6], - "data": [0.012342244386672974, 0.01096448302268982, 0.1158677339553833, 0.44694992899894714, 0.1286596953868866, 0.006717503070831299, 0.017133980989456177, 0.033284008502960205, 0.25810787081718445, 0.5552864074707031, 0.2737370431423187, 0.06647294759750366, 0.003308534622192383, 0.042094916105270386, 0.35035040974617004, 0.4123249650001526, 0.20221450924873352, 0.0890253484249115, 0.005402028560638428, 0.08932945132255554, 0.230217844247818, 0.10292461514472961, 0.06133195757865906, 0.014476627111434937, 0.03374573588371277, 0.1323091685771942, 0.14242789149284363, 0.07755574584007263, 0.06197690963745117, 0.009882509708404541, 0.060907602310180664, 0.15094509720802307, 0.3482201397418976, 0.5302376747131348, 0.2937738597393036, 0.035843729972839355, 0.04474979639053345, 0.044087767601013184, 0.21957549452781677, 0.5710055828094482, 0.1859424114227295, 0.007858902215957642, 0.049711674451828, 0.06468978524208069, 0.2614569067955017, 0.594623863697052, 0.3964097499847412, 0.11661979556083679, 0.018707841634750366, 0.06048917770385742, 0.22115272283554077, 0.4127575159072876, 0.42119860649108887, 0.3302615284919739, 0.02938532829284668, 0.1332528293132782, 0.18242979049682617, 0.13389477133750916, 0.17680612206459045, 0.12142497301101685, 0.1723994016647339, 0.3951329290866852, 0.24690353870391846, 0.07913082838058472, 0.08758294582366943, 0.05526810884475708, 0.4753569960594177, 0.6999930143356323, 0.567375123500824, 0.23302730917930603, 0.12633883953094482, 0.06496754288673401, 0.1641591489315033, 0.2411300539970398, 0.7386078834533691, 0.9474913477897644, 0.8248361945152283, 0.23328974843025208, 0.37045222520828247, 0.3393784165382385, 0.6302773952484131, 0.8694397211074829, 0.7613192796707153, 0.48684951663017273, 0.26559197902679443, 0.2582207918167114, 0.36008960008621216, 0.5539557933807373, 0.5419197082519531, 0.5212063789367676, 0.06451210379600525, 0.17286467552185059, 0.27314117550849915, 0.35451239347457886, 0.46266332268714905, 0.4913651645183563, 0.09803968667984009, 0.44402244687080383, 0.5385090112686157, 0.3561117649078369, 0.418980211019516, 0.4442629814147949, 0.5851818919181824, 0.9161800146102905, 0.8944555521011353, 0.49867022037506104, 0.3052222728729248, 0.2035333514213562, 0.1193169355392456, 0.2216779589653015, 0.9154506921768188, 0.9965488910675049, 0.9943029880523682, 0.9744165539741516, 0.5597403645515442, 0.4065163731575012, 0.7735046148300171, 0.967598557472229, 0.960934042930603, 0.9459958672523499, 0.42590710520744324, 0.29039525985717773, 0.3589380979537964, 0.5739474892616272, 0.5605837106704712, 0.6902213096618652, 0.016057342290878296, 0.06896945834159851, 0.24833816289901733, 0.4657638370990753, 0.5265678763389587, 0.6627694368362427, 0.005190074443817139, 0.10515579581260681, 0.5558987855911255, 0.713051438331604, 0.7280378937721252, 0.7046148180961609, 0.10354965925216675, 0.6807599663734436, 0.9175513982772827, 0.8773623108863831, 0.739881157875061, 0.37515419721603394, 0.14251708984375, 0.38087186217308044, 0.6770038604736328, 0.676689863204956, 0.2333480715751648, 0.01913246512413025, 0.6815141439437866, 0.7772707343101501, 0.7486706972122192, 0.4629620313644409, 0.1405007541179657, 0.027928531169891357, 0.9229927062988281, 0.9385423064231873, 0.8933441638946533, 0.42891475558280945, 0.13773223757743835, 0.10417571663856506, 0.8172311782836914, 0.9119154214859009, 0.9084517955780029, 0.32700300216674805, 0.047574013471603394, 0.05047231912612915, 0.18879976868629456, 0.4397132396697998, 0.6234279274940491, 0.21472185850143433, 0.02488693594932556, 0.02298593521118164, 0.009523391723632812, 0.03789523243904114, 0.20581254363059998, 0.29005858302116394, 0.08589288592338562, 0.10273277759552002, 0.25986742973327637, 0.5668396353721619, 0.6219808459281921, 0.25254449248313904, 0.052643269300460815, 0.030241698026657104, 0.6235252618789673, 0.7727090716362, 0.8265547752380371, 0.5283986926078796, 0.20181995630264282, 0.12831100821495056, 0.9073105454444885, 0.9405269622802734, 0.963839054107666, 0.8290628790855408, 0.48561784625053406, 0.38003212213516235, 0.8741525411605835, 0.9368342161178589, 0.9671202898025513, 0.7964122891426086, 0.27530381083488464, 0.20199304819107056, 0.5760143995285034, 0.6911416053771973, 0.8462609052658081, 0.6876096129417419, 0.21398693323135376, 0.18144112825393677, 0.24613118171691895, 0.2376994788646698, 0.5611480474472046, 0.761987566947937, 0.5438836216926575, 0.5934877991676331, 0.34567391872406006, 0.7076606750488281, 0.7571935653686523, 0.20586702227592468, 0.03491181135177612, 0.08860194683074951, 0.43043527007102966, 0.7111687660217285, 0.8584127426147461, 0.5320301651954651, 0.2507604956626892, 0.35066044330596924, 0.6837804913520813, 0.8676763772964478, 0.9612888693809509, 0.8712221384048462, 0.6352329254150391, 0.5637025833129883, 0.8191525936126709, 0.9235308170318604, 0.9762829542160034, 0.9002748727798462, 0.46361708641052246, 0.31800857186317444, 0.8347306251525879, 0.859768271446228, 0.9536126852035522, 0.9277721643447876, 0.6456021070480347, 0.5894713997840881, 0.7662683725357056, 0.6410989761352539, 0.9034184217453003, 0.978650689125061, 0.9692908525466919, 0.9727474451065063, 0.2593540549278259, 0.7335802912712097, 0.9067511558532715, 0.5687150359153748, 0.13704237341880798, 0.3826751112937927, 0.22359034419059753, 0.6463444828987122, 0.8996938467025757, 0.6673703193664551, 0.4222683310508728, 0.6655619144439697, 0.3873872756958008, 0.7544618844985962, 0.9408840537071228, 0.8350484371185303, 0.6535878777503967, 0.6674923300743103, 0.8122230768203735, 0.9196552038192749, 0.9753252267837524, 0.8985309600830078, 0.48122650384902954, 0.3422449827194214, 0.8536701202392578, 0.9048675894737244, 0.9780391454696655, 0.9615316390991211, 0.7922146320343018, 0.7831512689590454, 0.5730212926864624, 0.6722979545593262, 0.9697206616401672, 0.9954839944839478, 0.995079517364502, 0.9960567355155945] - } - }, - { - "id": "signed_classified", - "block": "signed", - "output_class": null, - "channels": [1], - "probability": { - "dtype": "float32", - "shape": [1, 4, 6, 6], - "data": [0.14251708984375, 0.38087186217308044, 0.6770038604736328, 0.676689863204956, 0.2333480715751648, 0.01913246512413025, 0.6815141439437866, 0.7772707343101501, 0.7486706972122192, 0.4629620313644409, 0.1405007541179657, 0.027928531169891357, 0.9229927062988281, 0.9385423064231873, 0.8933441638946533, 0.42891475558280945, 0.13773223757743835, 0.10417571663856506, 0.8172311782836914, 0.9119154214859009, 0.9084517955780029, 0.32700300216674805, 0.047574013471603394, 0.05047231912612915, 0.18879976868629456, 0.4397132396697998, 0.6234279274940491, 0.21472185850143433, 0.02488693594932556, 0.02298593521118164, 0.009523391723632812, 0.03789523243904114, 0.20581254363059998, 0.29005858302116394, 0.08589288592338562, 0.10273277759552002, 0.25986742973327637, 0.5668396353721619, 0.6219808459281921, 0.25254449248313904, 0.052643269300460815, 0.030241698026657104, 0.6235252618789673, 0.7727090716362, 0.8265547752380371, 0.5283986926078796, 0.20181995630264282, 0.12831100821495056, 0.9073105454444885, 0.9405269622802734, 0.963839054107666, 0.8290628790855408, 0.48561784625053406, 0.38003212213516235, 0.8741525411605835, 0.9368342161178589, 0.9671202898025513, 0.7964122891426086, 0.27530381083488464, 0.20199304819107056, 0.5760143995285034, 0.6911416053771973, 0.8462609052658081, 0.6876096129417419, 0.21398693323135376, 0.18144112825393677, 0.24613118171691895, 0.2376994788646698, 0.5611480474472046, 0.761987566947937, 0.5438836216926575, 0.5934877991676331, 0.34567391872406006, 0.7076606750488281, 0.7571935653686523, 0.20586702227592468, 0.03491181135177612, 0.08860194683074951, 0.43043527007102966, 0.7111687660217285, 0.8584127426147461, 0.5320301651954651, 0.2507604956626892, 0.35066044330596924, 0.6837804913520813, 0.8676763772964478, 0.9612888693809509, 0.8712221384048462, 0.6352329254150391, 0.5637025833129883, 0.8191525936126709, 0.9235308170318604, 0.9762829542160034, 0.9002748727798462, 0.46361708641052246, 0.31800857186317444, 0.8347306251525879, 0.859768271446228, 0.9536126852035522, 0.9277721643447876, 0.6456021070480347, 0.5894713997840881, 0.7662683725357056, 0.6410989761352539, 0.9034184217453003, 0.978650689125061, 0.9692908525466919, 0.9727474451065063, 0.2593540549278259, 0.7335802912712097, 0.9067511558532715, 0.5687150359153748, 0.13704237341880798, 0.3826751112937927, 0.22359034419059753, 0.6463444828987122, 0.8996938467025757, 0.6673703193664551, 0.4222683310508728, 0.6655619144439697, 0.3873872756958008, 0.7544618844985962, 0.9408840537071228, 0.8350484371185303, 0.6535878777503967, 0.6674923300743103, 0.8122230768203735, 0.9196552038192749, 0.9753252267837524, 0.8985309600830078, 0.48122650384902954, 0.3422449827194214, 0.8536701202392578, 0.9048675894737244, 0.9780391454696655, 0.9615316390991211, 0.7922146320343018, 0.7831512689590454, 0.5730212926864624, 0.6722979545593262, 0.9697206616401672, 0.9954839944839478, 0.995079517364502, 0.9960567355155945] - } - } - ], - "segment": [ - { - "id": "aff_default", - "method": "components", - "block": "aff", - "output_class": "unbounded", - "channels": [0, 1, 2], - "offsets": null, - "threshold": 0.5, - "min_size": 0, - "connectivity": 1, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [6, 10, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0] - }, - "count": 2 - }, - { - "id": "aff_low_edges", - "method": "components", - "block": "aff", - "output_class": "unbounded", - "channels": [0, 1, 2], - "offsets": null, - "threshold": 0.3, - "min_size": 0, - "connectivity": 2, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [6, 10, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1] - }, - "count": 1 - }, - { - "id": "aff_classified_all_min3", - "method": "components", - "block": "aff", - "output_class": null, - "channels": [0, 1, 2], - "offsets": null, - "threshold": 0.7, - "min_size": 3, - "connectivity": 3, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [6, 10, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 2, 2, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 2, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 2, 2, 2, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 2, 2, 0, 0, 0, 1, 1, 1, 0, 0, 1, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 2, 2, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 2, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0] - }, - "count": 2 - }, - { - "id": "aff_one_channel_per_slice", - "method": "components", - "block": "aff", - "output_class": "unbounded", - "channels": [1], - "offsets": null, - "threshold": 0.5, - "min_size": 0, - "connectivity": 1, - "per_slice": true, - "objects": { - "dtype": "uint64", - "shape": [6, 10, 12], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 2, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 7, 0, 0, 0, 0, 0, 6, 6, 6, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 6, 6, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 6, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7, 7, 0, 0, 0, 0, 0, 9, 9, 0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 0, 11, 11, 11, 11, 11, 11, 0, 10, 10, 0, 0, 0, 11, 11, 11, 11, 0, 0, 0, 10, 10, 10, 0, 0, 0, 11, 11, 0, 0, 0, 0, 10, 10, 10, 0, 0, 0, 11, 11, 0, 0, 0, 0, 10, 10, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 10, 10, 0, 0, 0, 0, 11, 11, 11, 0, 0, 12, 10, 10, 0, 0, 0, 0, 0, 11, 11, 0, 0, 12, 10, 10, 10, 0, 0, 0, 11, 11, 11, 0, 0, 12, 10, 10, 10, 0, 0, 0, 0, 11, 11, 0, 0, 12, 0, 10, 0, 0, 0, 0, 11, 11, 11, 0, 0, 0, 13, 0, 14, 0, 15, 15, 15, 15, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 0, 13, 0, 0, 0, 13, 0, 0, 0, 0, 13, 13, 13, 13, 13, 13, 13, 13, 0, 0, 0, 0, 0, 13, 13, 0, 0, 13, 13, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 13, 13, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 13, 13, 13, 13, 13, 16, 16, 0, 0, 0, 13, 13, 13, 13, 13, 13, 13, 16, 16, 16, 16, 0, 13, 13, 13, 13, 0, 13, 13, 0, 16, 16, 16, 0, 13, 13, 13, 13, 0, 0, 13, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 0, 0, 0, 0, 18, 18, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 17, 17, 0, 0, 0, 0, 19, 19, 0, 0, 0, 0, 0, 17, 17, 0, 19, 19, 19, 19, 0, 0, 0, 0, 0, 17, 0, 0, 0, 19, 19, 19, 19, 0, 19, 0, 0, 0, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19, 0, 0, 0, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 0, 19, 19, 19, 19, 19, 0, 19, 19, 19, 19, 19, 0, 0, 19, 20, 20, 20, 20, 20, 0, 0, 0, 21, 21, 0, 0, 20, 20, 0, 20, 0, 0, 0, 0, 21, 21, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 21, 21, 0, 0, 0, 20, 0, 0, 0, 0, 0, 21, 21, 21, 21, 0, 0, 0, 22, 0, 0, 0, 0, 21, 21, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21, 21, 21, 21, 0, 0, 0, 0, 21, 21, 21, 21, 21, 21, 21, 21, 0, 0, 0, 0, 21, 21, 21, 21, 21, 0, 21, 21, 23, 0, 0, 0, 21, 21, 21, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21, 21, 0, 0, 0, 0] - }, - "count": 23 - }, - { - "id": "binary_default", - "method": "components", - "block": "binary", - "output_class": "unbounded", - "channels": null, - "offsets": null, - "threshold": 0.5, - "min_size": 0, - "connectivity": 1, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [6, 9, 10], - "data": [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count": 3 - }, - { - "id": "binary_per_slice_min5", - "method": "components", - "block": "binary", - "output_class": "unbounded", - "channels": null, - "offsets": null, - "threshold": 0.5, - "min_size": 5, - "connectivity": 1, - "per_slice": true, - "objects": { - "dtype": "uint64", - "shape": [6, 9, 10], - "data": [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2, 1, 1, 0, 0, 0, 0, 0, 2, 2, 2, 1, 1, 0, 0, 0, 0, 2, 2, 2, 0, 1, 1, 1, 0, 0, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 0, 0, 0, 4, 4, 4, 0, 0, 4, 4, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 0, 0, 6, 6, 6, 6, 6, 6, 6, 0, 0, 0, 6, 6, 6, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count": 8 - }, - { - "id": "binary_per_slice_all", - "method": "components", - "block": "binary", - "output_class": "unbounded", - "channels": null, - "offsets": null, - "threshold": 0.6, - "min_size": 0, - "connectivity": 3, - "per_slice": true, - "objects": { - "dtype": "uint64", - "shape": [6, 9, 10], - "data": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 2, 1, 1, 0, 0, 0, 0, 0, 2, 2, 2, 1, 1, 0, 0, 0, 0, 2, 2, 2, 0, 1, 1, 0, 0, 0, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 0, 4, 3, 3, 3, 0, 0, 0, 0, 3, 0, 4, 3, 3, 3, 0, 0, 0, 0, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 0, 3, 0, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 0, 6, 6, 0, 0, 0, 5, 5, 0, 0, 0, 6, 6, 0, 0, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 0, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 0, 10, 10, 0, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 13, 13, 13, 15, 0, 0, 0, 0, 0, 0, 0, 13, 13, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "count": 18 - }, - { - "id": "unit_default", - "method": "components", - "block": "unit", - "output_class": "unit", - "channels": null, - "offsets": null, - "threshold": 0.5, - "min_size": 0, - "connectivity": 1, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [5, 6, 7], - "data": [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1] - }, - "count": 1 - }, - { - "id": "unit_classified_edges", - "method": "components", - "block": "unit", - "output_class": null, - "channels": null, - "offsets": null, - "threshold": 0.4, - "min_size": 0, - "connectivity": 2, - "per_slice": true, - "objects": { - "dtype": "uint64", - "shape": [5, 6, 7], - "data": [0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 0, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 0, 0, 3, 3, 3, 3, 3, 4, 4, 4, 4, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 4, 0, 4, 4, 4, 0, 0, 0, 4, 4, 4, 4, 0, 0, 4, 4, 4, 4, 4, 5, 5, 5, 5, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 5, 5, 5, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 5, 5, 5, 5] - }, - "count": 5 - }, - { - "id": "signed_default", - "method": "components", - "block": "signed", - "output_class": "signed_unit", - "channels": null, - "offsets": null, - "threshold": 0.5, - "min_size": 0, - "connectivity": 1, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [4, 6, 6], - "data": [0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1] - }, - "count": 1 - }, - { - "id": "signed_classified_min2", - "method": "components", - "block": "signed", - "output_class": null, - "channels": null, - "offsets": null, - "threshold": 0.6, - "min_size": 2, - "connectivity": 1, - "per_slice": false, - "objects": { - "dtype": "uint64", - "shape": [4, 6, 6], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1] - }, - "count": 1 - }, + "mutex_watershed": [ { "id": "mutex_default", - "method": "mutex_watershed", - "block": "aff_unit", "output_class": "unit", - "channels": [0, 1, 2], "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "threshold": 0.5, "min_size": 0, - "connectivity": 1, - "per_slice": false, + "affinities": { + "dtype": "float32", + "shape": [3, 4, 8, 9], + "data": [0.006405787542462349, 0.09727895259857178, 0.16088104248046875, 0.01466405764222145, 0.005348739679902792, 0.0009836857207119465, 0.0008501893607899547, 0.0007184659480117261, 0.00361567921936512, 0.023111745715141296, 0.4801984429359436, 0.7475154995918274, 0.29652276635169983, 0.11163563281297684, 0.03805999830365181, 0.0008071252377703786, 0.0002087654429487884, 0.00037951424019411206, 0.1419934779405594, 0.685640811920166, 0.8455166816711426, 0.6160955429077148, 0.2896983325481415, 0.6411769390106201, 0.04796143248677254, 0.010922696441411972, 0.0009256632765755057, 0.10980672389268875, 0.39533811807632446, 0.5900510549545288, 0.7259947657585144, 0.892128050327301, 0.9393881559371948, 0.12829263508319855, 0.016024377197027206, 0.013484541326761246, 0.022845521569252014, 0.009178868494927883, 0.38717421889305115, 0.9383704662322998, 0.9533053040504456, 0.9247043132781982, 0.35003551840782166, 0.20298410952091217, 0.12133221328258514, 0.019167760387063026, 0.02153478004038334, 0.23183898627758026, 0.9496617913246155, 0.9965312480926514, 0.9739004373550415, 0.9196115136146545, 0.8520665168762207, 0.7459754347801208, 0.19148880243301392, 0.7444937229156494, 0.9323265552520752, 0.9360714554786682, 0.898582398891449, 0.9626148343086243, 0.8647717833518982, 0.8427475690841675, 0.6785611510276794, 0.7403771877288818, 0.978455126285553, 0.9520742297172546, 0.891749382019043, 0.38169166445732117, 0.2778847813606262, 0.02232198230922222, 0.009397313930094242, 0.031109606847167015, 0.8451650142669678, 0.10122910141944885, 0.10893440991640091, 0.07132386416196823, 0.09226083010435104, 0.006728814914822578, 0.005487055517733097, 0.020825456827878952, 0.030349858105182648, 0.4935806691646576, 0.26552248001098633, 0.16372177004814148, 0.29161378741264343, 0.07623057067394257, 0.02803158573806286, 0.04092201218008995, 0.00999289471656084, 0.003432896686717868, 0.31116461753845215, 0.4003812372684479, 0.4084051549434662, 0.26420125365257263, 0.5042476654052734, 0.5529907941818237, 0.31194642186164856, 0.015944773331284523, 0.0103612607344985, 0.40324026346206665, 0.12452642619609833, 0.342197984457016, 0.6234222650527954, 0.7146673798561096, 0.6825367212295532, 0.13847388327121735, 0.023022405803203583, 0.001214761519804597, 0.1197289451956749, 0.039545439183712006, 0.2656973898410797, 0.7078984379768372, 0.8934940695762634, 0.3431282937526703, 0.02259020134806633, 0.005757425911724567, 0.0008348029223270714, 0.12116753309965134, 0.17550402879714966, 0.5276204347610474, 0.9808399081230164, 0.9827712774276733, 0.442697674036026, 0.2518978714942932, 0.07100124657154083, 0.026931291446089745, 0.7023043632507324, 0.8918875455856323, 0.9407371282577515, 0.9891384840011597, 0.9804701805114746, 0.744357705116272, 0.03922814503312111, 0.1006072610616684, 0.03492124006152153, 0.19476120173931122, 0.8906826972961426, 0.9940574765205383, 0.9975727200508118, 0.9124335050582886, 0.15608137845993042, 0.009714008308947086, 0.0005685868673026562, 0.0028838152065873146, 0.9684820175170898, 0.14863833785057068, 0.02140953578054905, 0.02522405982017517, 0.009544783271849155, 0.0035504119005054235, 0.0060817464254796505, 0.014942399226129055, 0.016097957268357277, 0.9338895082473755, 0.2003743052482605, 0.07766573131084442, 0.09010124951601028, 0.15433838963508606, 0.028244270011782646, 0.015032581053674221, 0.022749900817871094, 0.007086070254445076, 0.713589608669281, 0.4698563814163208, 0.24384739995002747, 0.5995345711708069, 0.2422187477350235, 0.1000564694404602, 0.06074699014425278, 0.0553671233355999, 0.019963808357715607, 0.32123124599456787, 0.19557100534439087, 0.1437099725008011, 0.7205166220664978, 0.6229304671287537, 0.20681019127368927, 0.06795535236597061, 0.018874960020184517, 0.003169267438352108, 0.29055291414260864, 0.22719217836856842, 0.5093474388122559, 0.46589040756225586, 0.6188870072364807, 0.2447742521762848, 0.02386835403740406, 0.0007393567357212305, 8.486895239911973e-05, 0.49906954169273376, 0.6174333691596985, 0.4286477863788605, 0.9189798831939697, 0.9521164298057556, 0.5627703070640564, 0.053999677300453186, 0.009448239579796791, 0.0013868428068235517, 0.8636879324913025, 0.5927143692970276, 0.7062453031539917, 0.991646409034729, 0.9522122144699097, 0.698702335357666, 0.14986637234687805, 0.032975345849990845, 0.012944228947162628, 0.24933764338493347, 0.5741854906082153, 0.9455171227455139, 0.9977059364318848, 0.9338324666023254, 0.24771615862846375, 0.003724754089489579, 0.0009586532833054662, 0.002418813295662403, 0.16659937798976898, 0.14725081622600555, 0.034873832017183304, 0.00478315306827426, 0.007050607353448868, 0.0012430199421942234, 0.0014806532999500632, 0.0017984578153118491, 0.007145723793655634, 0.5294622778892517, 0.7156361937522888, 0.12347240746021271, 0.07190761715173721, 0.029341809451580048, 0.0037194425240159035, 0.0024966285564005375, 0.016582781448960304, 0.008220940828323364, 0.8644982576370239, 0.774003267288208, 0.927916944026947, 0.6397309303283691, 0.14958657324314117, 0.01809884048998356, 0.015695104375481606, 0.06346869468688965, 0.02550654485821724, 0.7866106033325195, 0.7283308506011963, 0.9321506023406982, 0.9482742547988892, 0.902815580368042, 0.36903971433639526, 0.07919389754533768, 0.014410452917218208, 0.0016151153249666095, 0.15568168461322784, 0.6373230814933777, 0.8571060299873352, 0.7333729863166809, 0.746910035610199, 0.7672332525253296, 0.15452739596366882, 0.0009892996167764068, 0.00013574546028394252, 0.8373446464538574, 0.5875785946846008, 0.6509594321250916, 0.659740686416626, 0.8974853754043579, 0.8198027014732361, 0.21425434947013855, 0.016756094992160797, 0.001395352534018457, 0.9202431440353394, 0.8624597787857056, 0.9555229544639587, 0.9267820715904236, 0.9297482371330261, 0.8606886267662048, 0.35940679907798767, 0.08139093220233917, 0.1542719602584839, 0.1797739416360855, 0.5315520763397217, 0.9590945243835449, 0.9896495938301086, 0.9228262305259705, 0.10271875560283661, 0.002909652190282941, 0.020892489701509476, 0.37895193696022034, 0.036463551223278046, 0.27709120512008667, 0.23738320171833038, 0.024375230073928833, 0.014955307357013226, 0.004074228461831808, 0.0009376180241815746, 0.00029187617474235594, 0.0010817673755809665, 0.017200732603669167, 0.4444396197795868, 0.7992346882820129, 0.1523294299840927, 0.02471093088388443, 0.011450223624706268, 0.002722658682614565, 0.00016372262325603515, 0.0014774196315556765, 0.47880834341049194, 0.8638392686843872, 0.8548949956893921, 0.3515254557132721, 0.5757929086685181, 0.4932696521282196, 0.07410646229982376, 0.0048089949414134026, 0.00622129812836647, 0.3016401529312134, 0.5596623420715332, 0.3977149426937103, 0.2782095670700073, 0.8331773281097412, 0.9201893210411072, 0.3025350272655487, 0.05041160434484482, 0.010621345601975918, 0.015816206112504005, 0.017386458814144135, 0.0777549296617508, 0.8212324976921082, 0.9859774708747864, 0.748892068862915, 0.4023588001728058, 0.20803196728229523, 0.09152422845363617, 0.02683401107788086, 0.018570035696029663, 0.09454040229320526, 0.9009758830070496, 0.9955010414123535, 0.9845141768455505, 0.7415720224380493, 0.9277499914169312, 0.4548429548740387, 0.29804039001464844, 0.6117668747901917, 0.7855589985847473, 0.9545477032661438, 0.958476185798645, 0.9272050857543945, 0.8883187770843506, 0.858760416507721, 0.8754853010177612, 0.3832662105560303, 0.9524270296096802, 0.9616305828094482, 0.8907509446144104, 0.3620733916759491, 0.10983877629041672, 0.04657716676592827, 0.014462055638432503, 0.02250339463353157, 0.7154629230499268, 0.10569366812705994, 0.14334894716739655, 0.08471138775348663, 0.014225919730961323, 0.00822182185947895, 0.004308593459427357, 0.03918813169002533, 0.03596852719783783, 0.6235819458961487, 0.40530654788017273, 0.282954603433609, 0.12767809629440308, 0.08226874470710754, 0.06275758892297745, 0.03349435329437256, 0.0038431594148278236, 0.01502765342593193, 0.17439816892147064, 0.11114143580198288, 0.5317608714103699, 0.6187898516654968, 0.44940507411956787, 0.4582734704017639, 0.04580692946910858, 0.03119811974465847, 0.005379194859415293, 0.16011156141757965, 0.27635979652404785, 0.2144898921251297, 0.805109441280365, 0.7985683679580688, 0.38571861386299133, 0.06431533396244049, 0.025711029767990112, 0.0037043134216219187, 0.08650750666856766, 0.09820803999900818, 0.1143355742096901, 0.6504985690116882, 0.9214207530021667, 0.3625200092792511, 0.021512825042009354, 0.003598347771912813, 0.0007117267232388258, 0.40182918310165405, 0.29010674357414246, 0.6876466870307922, 0.9573358297348022, 0.980923593044281, 0.5157013535499573, 0.10556859523057938, 0.06603454053401947, 0.010391706600785255, 0.31896814703941345, 0.8630676865577698, 0.9674144983291626, 0.9894375801086426, 0.942147433757782, 0.5651320815086365, 0.23744112253189087, 0.0765782818198204, 0.037936434149742126, 0.24656705558300018, 0.9755046963691711, 0.987320065498352, 0.9973815679550171, 0.9733712077140808, 0.15568718314170837, 0.0014592820079997182, 0.0007763439207337797, 0.003524448722600937, 0.9712328910827637, 0.3113802671432495, 0.04120058938860893, 0.029378242790699005, 0.009301147423684597, 0.01962519809603691, 0.006872847210615873, 0.03432290628552437, 0.02574305236339569, 0.9438517093658447, 0.3803180158138275, 0.016599861904978752, 0.046117398887872696, 0.08080612123012543, 0.01782093569636345, 0.010331135243177414, 0.015400949865579605, 0.016450047492980957, 0.7294918298721313, 0.59328293800354, 0.15634390711784363, 0.5180606842041016, 0.41202107071876526, 0.21463340520858765, 0.022310873493552208, 0.07038745284080505, 0.007681288756430149, 0.10690692067146301, 0.18599335849285126, 0.4049161672592163, 0.7779960036277771, 0.6279895901679993, 0.3181595206260681, 0.07276221364736557, 0.006887141149491072, 0.0009561369079165161, 0.3169105350971222, 0.2076488882303238, 0.4842545986175537, 0.6714500784873962, 0.6789632439613342, 0.3211117088794708, 0.0303569994866848, 0.0003437900741118938, 0.000224252202315256, 0.7955291271209717, 0.6867148280143738, 0.7030497789382935, 0.6080354452133179, 0.9137871265411377, 0.40626654028892517, 0.08090484887361526, 0.005014144349843264, 0.0008612490491941571, 0.43387120962142944, 0.78774493932724, 0.8998064398765564, 0.9974437952041626, 0.9735685586929321, 0.9130439758300781, 0.10426246374845505, 0.07976429164409637, 0.006124634295701981, 0.22978587448596954, 0.6298061609268188, 0.9497805237770081, 0.9979004859924316, 0.9811471104621887, 0.11227890849113464, 0.009679608978331089, 0.0015625111991539598, 0.005984739866107702, 0.3933556377887726, 0.07433469593524933, 0.073813296854496, 0.025041161105036736, 0.003479322884231806, 0.0027101568412035704, 0.003297708462923765, 0.0036330465227365494, 0.005828320048749447, 0.8169421553611755, 0.3659225404262543, 0.23431475460529327, 0.06277850270271301, 0.011350726708769798, 0.002585472073405981, 0.004223544616252184, 0.0055309501476585865, 0.011714592576026917, 0.6990140080451965, 0.7143499851226807, 0.8165850043296814, 0.7749465107917786, 0.18483403325080872, 0.01825827546417713, 0.019674180075526237, 0.06838100403547287, 0.08795469254255295, 0.7525714635848999, 0.7650346755981445, 0.8397188782691956, 0.9127547144889832, 0.7892559766769409, 0.2669185400009155, 0.08987037092447281, 0.03852618485689163, 0.003076256485655904, 0.21000468730926514, 0.449381023645401, 0.5557305216789246, 0.7972224354743958, 0.9337754249572754, 0.8973298668861389, 0.125515416264534, 0.0030785175040364265, 0.000563664420042187, 0.7774319648742676, 0.8161802291870117, 0.7318100929260254, 0.6054673790931702, 0.8640800714492798, 0.8725725412368774, 0.4066160321235657, 0.008981174789369106, 0.001729608979076147, 0.5481672883033752, 0.7885011434555054, 0.7658112645149231, 0.935683012008667, 0.9413362741470337, 0.4633576571941376, 0.29008156061172485, 0.07462448626756668, 0.04937174171209335, 0.3021053969860077, 0.4201965630054474, 0.9003274440765381, 0.9926263689994812, 0.9480562210083008, 0.042369596660137177, 0.005779182072728872, 0.008414193987846375, 0.24217785894870758, 0.0155562749132514, 0.09378772974014282, 0.16245222091674805, 0.03054456226527691, 0.002467889804393053, 0.004624940454959869, 0.0005982423317618668, 0.0003144068468827754, 0.000854213023558259, 0.0270580742508173, 0.14774569869041443, 0.6388610601425171, 0.240841343998909, 0.05799445882439613, 0.013676602393388748, 0.0007921734941191971, 0.000643699080683291, 0.001191712450236082, 0.13431710004806519, 0.8179196119308472, 0.6153555512428284, 0.35775843262672424, 0.5316541194915771, 0.5420814752578735, 0.06136036664247513, 0.003946713637560606, 0.003959944471716881, 0.13619516789913177, 0.40717384219169617, 0.4833129942417145, 0.3982491195201874, 0.7812288403511047, 0.9762170314788818, 0.13124923408031464, 0.056378304958343506, 0.02727123349905014, 0.02908400632441044, 0.03789268434047699, 0.18035878241062164, 0.7356791496276855, 0.9755395650863647, 0.8796552419662476, 0.32139745354652405, 0.25075605511665344, 0.054308950901031494, 0.011015450581908226, 0.0145757170394063, 0.17323996126651764, 0.9549662470817566, 0.9900804758071899, 0.9780991673469543, 0.904492974281311, 0.8705196380615234, 0.7373524308204651, 0.12912921607494354, 0.42098432779312134, 0.7970767617225647, 0.9468807578086853, 0.8865792155265808, 0.8455470204353333, 0.8828883171081543, 0.8131395578384399, 0.9220535755157471, 0.6693574786186218, 0.987352192401886, 0.965021550655365, 0.9445539712905884, 0.1642608493566513, 0.06971129775047302, 0.03678398206830025, 0.007050120271742344, 0.01761852763593197, 0.6688684821128845, 0.2852058410644531, 0.0315309576690197, 0.113181933760643, 0.018022330477833748, 0.027434615418314934, 0.01194030325859785, 0.01684282347559929, 0.07791317999362946, 0.22980593144893646, 0.35293638706207275, 0.25983723998069763, 0.07790030539035797, 0.34559959173202515, 0.08268477767705917, 0.021001406013965607, 0.013827490620315075, 0.002802387112751603, 0.37692463397979736, 0.35001325607299805, 0.4386589825153351, 0.3759610056877136, 0.3638794720172882, 0.4363403916358948, 0.24463322758674622, 0.016175279393792152, 0.014034891501069069, 0.15634796023368835, 0.21940714120864868, 0.31649571657180786, 0.763617753982544, 0.8242884874343872, 0.5883968472480774, 0.06002708896994591, 0.021962294355034828, 0.0024008776526898146, 0.05870017036795616, 0.06495744735002518, 0.2546616494655609, 0.5586676001548767, 0.8368721604347229, 0.04912634938955307, 0.019776765257120132, 0.013286319561302662, 0.0005793613381683826, 0.3900282084941864, 0.26565492153167725, 0.5160230994224548, 0.9372599720954895, 0.9749610424041748, 0.402459055185318, 0.1461663395166397, 0.017872918397188187, 0.016474133357405663, 0.4194425046443939, 0.7804387211799622, 0.9433783292770386, 0.9895066022872925, 0.9787696599960327, 0.5724721550941467, 0.1809379756450653, 0.15044806897640228, 0.03902287036180496, 0.6065672636032104, 0.9183623790740967, 0.9981423020362854, 0.9925916790962219, 0.8051732182502747, 0.2920267581939697, 0.004329930990934372, 0.001743402099236846, 0.004750567954033613, 0.9734861254692078, 0.1374809741973877, 0.05401984229683876, 0.01837339624762535, 0.0163206048309803, 0.007249115966260433, 0.008688676171004772, 0.018351685255765915, 0.02164692059159279, 0.8242896199226379, 0.10578954964876175, 0.03890813887119293, 0.13291649520397186, 0.11650087684392929, 0.022387361153960228, 0.015191460028290749, 0.03175981715321541, 0.011223052628338337, 0.7934597134590149, 0.38556817173957825, 0.19115711748600006, 0.5173042416572571, 0.28223028779029846, 0.2483171820640564, 0.10793901234865189, 0.05800192803144455, 0.010297705419361591, 0.29882875084877014, 0.10554193705320358, 0.30510979890823364, 0.5615017414093018, 0.6499015688896179, 0.17074143886566162, 0.06733200699090958, 0.007534055970609188, 0.00368851819075644, 0.3178832232952118, 0.11489070951938629, 0.15339593589305878, 0.5811575651168823, 0.7124435305595398, 0.21938546001911163, 0.017117254436016083, 0.00091215455904603, 0.00016626961587462574, 0.7823334336280823, 0.5564435720443726, 0.7459773421287537, 0.9428247213363647, 0.906158447265625, 0.46177005767822266, 0.10572976619005203, 0.023506345227360725, 0.0010004211217164993, 0.7730580568313599, 0.4708198308944702, 0.9357206225395203, 0.988322377204895, 0.987572431564331, 0.4292345643043518, 0.15406490862369537, 0.05628373101353645, 0.011329490691423416, 0.15766359865665436, 0.8421526551246643, 0.9790674448013306, 0.9984972476959229, 0.9186737537384033, 0.22270335257053375, 0.004889292176812887, 0.0021661773789674044, 0.007955409586429596, 0.26999497413635254, 0.05651780962944031, 0.05046220123767853, 0.014368278905749321, 0.002416253322735429, 0.0007103332900442183, 0.003783422289416194, 0.003908967133611441, 0.006160776596516371, 0.5786407589912415, 0.771623969078064, 0.1652519553899765, 0.08877858519554138, 0.010129357688128948, 0.004066316410899162, 0.006653024815022945, 0.010777621529996395, 0.015371684916317463, 0.9239349961280823, 0.8420916795730591, 0.6621816158294678, 0.48380404710769653, 0.23091989755630493, 0.018160413950681686, 0.04436581954360008, 0.03655540943145752, 0.09200375527143478, 0.5385303497314453, 0.6273159980773926, 0.8137938380241394, 0.9626559615135193, 0.8859420418739319, 0.4924725592136383, 0.14467966556549072, 0.048656582832336426, 0.011090707033872604, 0.2822672724723816, 0.4796997904777527, 0.44503769278526306, 0.5951470136642456, 0.8819200992584229, 0.8180304765701294, 0.11254142224788666, 0.0019173943437635899, 0.00026608200278133154, 0.862504243850708, 0.4899362325668335, 0.5042241215705872, 0.7237051129341125, 0.9175408482551575, 0.8992620706558228, 0.17709442973136902, 0.026629246771335602, 0.0007309389184229076, 0.6920908689498901, 0.8351729512214661, 0.7759986519813538, 0.9384331107139587, 0.9568563103675842, 0.6665611863136292, 0.1466618925333023, 0.07675610482692719, 0.07741868495941162, 0.5778765082359314, 0.4013153910636902, 0.9383063912391663, 0.9883129000663757, 0.887401819229126, 0.046116285026073456, 0.0028072765562683344, 0.022630997002124786, 0.19556643068790436] + }, "objects": { "dtype": "uint64", "shape": [4, 8, 9], - "data": [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 3, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 0, 3, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 0, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 0, 0, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 2, 2, 2, 0, 0, 1, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, 0, 2, 2, 2, 2, 2, 1, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2] + "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0] }, - "count": 3 + "count": 1 }, { "id": "mutex_low", - "method": "mutex_watershed", - "block": "aff_unit", "output_class": "unit", - "channels": [0, 1, 2], "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "threshold": 0.3, "min_size": 0, - "connectivity": 1, - "per_slice": false, + "affinities": { + "dtype": "float32", + "shape": [3, 4, 8, 9], + "data": [0.006405787542462349, 0.09727895259857178, 0.16088104248046875, 0.01466405764222145, 0.005348739679902792, 0.0009836857207119465, 0.0008501893607899547, 0.0007184659480117261, 0.00361567921936512, 0.023111745715141296, 0.4801984429359436, 0.7475154995918274, 0.29652276635169983, 0.11163563281297684, 0.03805999830365181, 0.0008071252377703786, 0.0002087654429487884, 0.00037951424019411206, 0.1419934779405594, 0.685640811920166, 0.8455166816711426, 0.6160955429077148, 0.2896983325481415, 0.6411769390106201, 0.04796143248677254, 0.010922696441411972, 0.0009256632765755057, 0.10980672389268875, 0.39533811807632446, 0.5900510549545288, 0.7259947657585144, 0.892128050327301, 0.9393881559371948, 0.12829263508319855, 0.016024377197027206, 0.013484541326761246, 0.022845521569252014, 0.009178868494927883, 0.38717421889305115, 0.9383704662322998, 0.9533053040504456, 0.9247043132781982, 0.35003551840782166, 0.20298410952091217, 0.12133221328258514, 0.019167760387063026, 0.02153478004038334, 0.23183898627758026, 0.9496617913246155, 0.9965312480926514, 0.9739004373550415, 0.9196115136146545, 0.8520665168762207, 0.7459754347801208, 0.19148880243301392, 0.7444937229156494, 0.9323265552520752, 0.9360714554786682, 0.898582398891449, 0.9626148343086243, 0.8647717833518982, 0.8427475690841675, 0.6785611510276794, 0.7403771877288818, 0.978455126285553, 0.9520742297172546, 0.891749382019043, 0.38169166445732117, 0.2778847813606262, 0.02232198230922222, 0.009397313930094242, 0.031109606847167015, 0.8451650142669678, 0.10122910141944885, 0.10893440991640091, 0.07132386416196823, 0.09226083010435104, 0.006728814914822578, 0.005487055517733097, 0.020825456827878952, 0.030349858105182648, 0.4935806691646576, 0.26552248001098633, 0.16372177004814148, 0.29161378741264343, 0.07623057067394257, 0.02803158573806286, 0.04092201218008995, 0.00999289471656084, 0.003432896686717868, 0.31116461753845215, 0.4003812372684479, 0.4084051549434662, 0.26420125365257263, 0.5042476654052734, 0.5529907941818237, 0.31194642186164856, 0.015944773331284523, 0.0103612607344985, 0.40324026346206665, 0.12452642619609833, 0.342197984457016, 0.6234222650527954, 0.7146673798561096, 0.6825367212295532, 0.13847388327121735, 0.023022405803203583, 0.001214761519804597, 0.1197289451956749, 0.039545439183712006, 0.2656973898410797, 0.7078984379768372, 0.8934940695762634, 0.3431282937526703, 0.02259020134806633, 0.005757425911724567, 0.0008348029223270714, 0.12116753309965134, 0.17550402879714966, 0.5276204347610474, 0.9808399081230164, 0.9827712774276733, 0.442697674036026, 0.2518978714942932, 0.07100124657154083, 0.026931291446089745, 0.7023043632507324, 0.8918875455856323, 0.9407371282577515, 0.9891384840011597, 0.9804701805114746, 0.744357705116272, 0.03922814503312111, 0.1006072610616684, 0.03492124006152153, 0.19476120173931122, 0.8906826972961426, 0.9940574765205383, 0.9975727200508118, 0.9124335050582886, 0.15608137845993042, 0.009714008308947086, 0.0005685868673026562, 0.0028838152065873146, 0.9684820175170898, 0.14863833785057068, 0.02140953578054905, 0.02522405982017517, 0.009544783271849155, 0.0035504119005054235, 0.0060817464254796505, 0.014942399226129055, 0.016097957268357277, 0.9338895082473755, 0.2003743052482605, 0.07766573131084442, 0.09010124951601028, 0.15433838963508606, 0.028244270011782646, 0.015032581053674221, 0.022749900817871094, 0.007086070254445076, 0.713589608669281, 0.4698563814163208, 0.24384739995002747, 0.5995345711708069, 0.2422187477350235, 0.1000564694404602, 0.06074699014425278, 0.0553671233355999, 0.019963808357715607, 0.32123124599456787, 0.19557100534439087, 0.1437099725008011, 0.7205166220664978, 0.6229304671287537, 0.20681019127368927, 0.06795535236597061, 0.018874960020184517, 0.003169267438352108, 0.29055291414260864, 0.22719217836856842, 0.5093474388122559, 0.46589040756225586, 0.6188870072364807, 0.2447742521762848, 0.02386835403740406, 0.0007393567357212305, 8.486895239911973e-05, 0.49906954169273376, 0.6174333691596985, 0.4286477863788605, 0.9189798831939697, 0.9521164298057556, 0.5627703070640564, 0.053999677300453186, 0.009448239579796791, 0.0013868428068235517, 0.8636879324913025, 0.5927143692970276, 0.7062453031539917, 0.991646409034729, 0.9522122144699097, 0.698702335357666, 0.14986637234687805, 0.032975345849990845, 0.012944228947162628, 0.24933764338493347, 0.5741854906082153, 0.9455171227455139, 0.9977059364318848, 0.9338324666023254, 0.24771615862846375, 0.003724754089489579, 0.0009586532833054662, 0.002418813295662403, 0.16659937798976898, 0.14725081622600555, 0.034873832017183304, 0.00478315306827426, 0.007050607353448868, 0.0012430199421942234, 0.0014806532999500632, 0.0017984578153118491, 0.007145723793655634, 0.5294622778892517, 0.7156361937522888, 0.12347240746021271, 0.07190761715173721, 0.029341809451580048, 0.0037194425240159035, 0.0024966285564005375, 0.016582781448960304, 0.008220940828323364, 0.8644982576370239, 0.774003267288208, 0.927916944026947, 0.6397309303283691, 0.14958657324314117, 0.01809884048998356, 0.015695104375481606, 0.06346869468688965, 0.02550654485821724, 0.7866106033325195, 0.7283308506011963, 0.9321506023406982, 0.9482742547988892, 0.902815580368042, 0.36903971433639526, 0.07919389754533768, 0.014410452917218208, 0.0016151153249666095, 0.15568168461322784, 0.6373230814933777, 0.8571060299873352, 0.7333729863166809, 0.746910035610199, 0.7672332525253296, 0.15452739596366882, 0.0009892996167764068, 0.00013574546028394252, 0.8373446464538574, 0.5875785946846008, 0.6509594321250916, 0.659740686416626, 0.8974853754043579, 0.8198027014732361, 0.21425434947013855, 0.016756094992160797, 0.001395352534018457, 0.9202431440353394, 0.8624597787857056, 0.9555229544639587, 0.9267820715904236, 0.9297482371330261, 0.8606886267662048, 0.35940679907798767, 0.08139093220233917, 0.1542719602584839, 0.1797739416360855, 0.5315520763397217, 0.9590945243835449, 0.9896495938301086, 0.9228262305259705, 0.10271875560283661, 0.002909652190282941, 0.020892489701509476, 0.37895193696022034, 0.036463551223278046, 0.27709120512008667, 0.23738320171833038, 0.024375230073928833, 0.014955307357013226, 0.004074228461831808, 0.0009376180241815746, 0.00029187617474235594, 0.0010817673755809665, 0.017200732603669167, 0.4444396197795868, 0.7992346882820129, 0.1523294299840927, 0.02471093088388443, 0.011450223624706268, 0.002722658682614565, 0.00016372262325603515, 0.0014774196315556765, 0.47880834341049194, 0.8638392686843872, 0.8548949956893921, 0.3515254557132721, 0.5757929086685181, 0.4932696521282196, 0.07410646229982376, 0.0048089949414134026, 0.00622129812836647, 0.3016401529312134, 0.5596623420715332, 0.3977149426937103, 0.2782095670700073, 0.8331773281097412, 0.9201893210411072, 0.3025350272655487, 0.05041160434484482, 0.010621345601975918, 0.015816206112504005, 0.017386458814144135, 0.0777549296617508, 0.8212324976921082, 0.9859774708747864, 0.748892068862915, 0.4023588001728058, 0.20803196728229523, 0.09152422845363617, 0.02683401107788086, 0.018570035696029663, 0.09454040229320526, 0.9009758830070496, 0.9955010414123535, 0.9845141768455505, 0.7415720224380493, 0.9277499914169312, 0.4548429548740387, 0.29804039001464844, 0.6117668747901917, 0.7855589985847473, 0.9545477032661438, 0.958476185798645, 0.9272050857543945, 0.8883187770843506, 0.858760416507721, 0.8754853010177612, 0.3832662105560303, 0.9524270296096802, 0.9616305828094482, 0.8907509446144104, 0.3620733916759491, 0.10983877629041672, 0.04657716676592827, 0.014462055638432503, 0.02250339463353157, 0.7154629230499268, 0.10569366812705994, 0.14334894716739655, 0.08471138775348663, 0.014225919730961323, 0.00822182185947895, 0.004308593459427357, 0.03918813169002533, 0.03596852719783783, 0.6235819458961487, 0.40530654788017273, 0.282954603433609, 0.12767809629440308, 0.08226874470710754, 0.06275758892297745, 0.03349435329437256, 0.0038431594148278236, 0.01502765342593193, 0.17439816892147064, 0.11114143580198288, 0.5317608714103699, 0.6187898516654968, 0.44940507411956787, 0.4582734704017639, 0.04580692946910858, 0.03119811974465847, 0.005379194859415293, 0.16011156141757965, 0.27635979652404785, 0.2144898921251297, 0.805109441280365, 0.7985683679580688, 0.38571861386299133, 0.06431533396244049, 0.025711029767990112, 0.0037043134216219187, 0.08650750666856766, 0.09820803999900818, 0.1143355742096901, 0.6504985690116882, 0.9214207530021667, 0.3625200092792511, 0.021512825042009354, 0.003598347771912813, 0.0007117267232388258, 0.40182918310165405, 0.29010674357414246, 0.6876466870307922, 0.9573358297348022, 0.980923593044281, 0.5157013535499573, 0.10556859523057938, 0.06603454053401947, 0.010391706600785255, 0.31896814703941345, 0.8630676865577698, 0.9674144983291626, 0.9894375801086426, 0.942147433757782, 0.5651320815086365, 0.23744112253189087, 0.0765782818198204, 0.037936434149742126, 0.24656705558300018, 0.9755046963691711, 0.987320065498352, 0.9973815679550171, 0.9733712077140808, 0.15568718314170837, 0.0014592820079997182, 0.0007763439207337797, 0.003524448722600937, 0.9712328910827637, 0.3113802671432495, 0.04120058938860893, 0.029378242790699005, 0.009301147423684597, 0.01962519809603691, 0.006872847210615873, 0.03432290628552437, 0.02574305236339569, 0.9438517093658447, 0.3803180158138275, 0.016599861904978752, 0.046117398887872696, 0.08080612123012543, 0.01782093569636345, 0.010331135243177414, 0.015400949865579605, 0.016450047492980957, 0.7294918298721313, 0.59328293800354, 0.15634390711784363, 0.5180606842041016, 0.41202107071876526, 0.21463340520858765, 0.022310873493552208, 0.07038745284080505, 0.007681288756430149, 0.10690692067146301, 0.18599335849285126, 0.4049161672592163, 0.7779960036277771, 0.6279895901679993, 0.3181595206260681, 0.07276221364736557, 0.006887141149491072, 0.0009561369079165161, 0.3169105350971222, 0.2076488882303238, 0.4842545986175537, 0.6714500784873962, 0.6789632439613342, 0.3211117088794708, 0.0303569994866848, 0.0003437900741118938, 0.000224252202315256, 0.7955291271209717, 0.6867148280143738, 0.7030497789382935, 0.6080354452133179, 0.9137871265411377, 0.40626654028892517, 0.08090484887361526, 0.005014144349843264, 0.0008612490491941571, 0.43387120962142944, 0.78774493932724, 0.8998064398765564, 0.9974437952041626, 0.9735685586929321, 0.9130439758300781, 0.10426246374845505, 0.07976429164409637, 0.006124634295701981, 0.22978587448596954, 0.6298061609268188, 0.9497805237770081, 0.9979004859924316, 0.9811471104621887, 0.11227890849113464, 0.009679608978331089, 0.0015625111991539598, 0.005984739866107702, 0.3933556377887726, 0.07433469593524933, 0.073813296854496, 0.025041161105036736, 0.003479322884231806, 0.0027101568412035704, 0.003297708462923765, 0.0036330465227365494, 0.005828320048749447, 0.8169421553611755, 0.3659225404262543, 0.23431475460529327, 0.06277850270271301, 0.011350726708769798, 0.002585472073405981, 0.004223544616252184, 0.0055309501476585865, 0.011714592576026917, 0.6990140080451965, 0.7143499851226807, 0.8165850043296814, 0.7749465107917786, 0.18483403325080872, 0.01825827546417713, 0.019674180075526237, 0.06838100403547287, 0.08795469254255295, 0.7525714635848999, 0.7650346755981445, 0.8397188782691956, 0.9127547144889832, 0.7892559766769409, 0.2669185400009155, 0.08987037092447281, 0.03852618485689163, 0.003076256485655904, 0.21000468730926514, 0.449381023645401, 0.5557305216789246, 0.7972224354743958, 0.9337754249572754, 0.8973298668861389, 0.125515416264534, 0.0030785175040364265, 0.000563664420042187, 0.7774319648742676, 0.8161802291870117, 0.7318100929260254, 0.6054673790931702, 0.8640800714492798, 0.8725725412368774, 0.4066160321235657, 0.008981174789369106, 0.001729608979076147, 0.5481672883033752, 0.7885011434555054, 0.7658112645149231, 0.935683012008667, 0.9413362741470337, 0.4633576571941376, 0.29008156061172485, 0.07462448626756668, 0.04937174171209335, 0.3021053969860077, 0.4201965630054474, 0.9003274440765381, 0.9926263689994812, 0.9480562210083008, 0.042369596660137177, 0.005779182072728872, 0.008414193987846375, 0.24217785894870758, 0.0155562749132514, 0.09378772974014282, 0.16245222091674805, 0.03054456226527691, 0.002467889804393053, 0.004624940454959869, 0.0005982423317618668, 0.0003144068468827754, 0.000854213023558259, 0.0270580742508173, 0.14774569869041443, 0.6388610601425171, 0.240841343998909, 0.05799445882439613, 0.013676602393388748, 0.0007921734941191971, 0.000643699080683291, 0.001191712450236082, 0.13431710004806519, 0.8179196119308472, 0.6153555512428284, 0.35775843262672424, 0.5316541194915771, 0.5420814752578735, 0.06136036664247513, 0.003946713637560606, 0.003959944471716881, 0.13619516789913177, 0.40717384219169617, 0.4833129942417145, 0.3982491195201874, 0.7812288403511047, 0.9762170314788818, 0.13124923408031464, 0.056378304958343506, 0.02727123349905014, 0.02908400632441044, 0.03789268434047699, 0.18035878241062164, 0.7356791496276855, 0.9755395650863647, 0.8796552419662476, 0.32139745354652405, 0.25075605511665344, 0.054308950901031494, 0.011015450581908226, 0.0145757170394063, 0.17323996126651764, 0.9549662470817566, 0.9900804758071899, 0.9780991673469543, 0.904492974281311, 0.8705196380615234, 0.7373524308204651, 0.12912921607494354, 0.42098432779312134, 0.7970767617225647, 0.9468807578086853, 0.8865792155265808, 0.8455470204353333, 0.8828883171081543, 0.8131395578384399, 0.9220535755157471, 0.6693574786186218, 0.987352192401886, 0.965021550655365, 0.9445539712905884, 0.1642608493566513, 0.06971129775047302, 0.03678398206830025, 0.007050120271742344, 0.01761852763593197, 0.6688684821128845, 0.2852058410644531, 0.0315309576690197, 0.113181933760643, 0.018022330477833748, 0.027434615418314934, 0.01194030325859785, 0.01684282347559929, 0.07791317999362946, 0.22980593144893646, 0.35293638706207275, 0.25983723998069763, 0.07790030539035797, 0.34559959173202515, 0.08268477767705917, 0.021001406013965607, 0.013827490620315075, 0.002802387112751603, 0.37692463397979736, 0.35001325607299805, 0.4386589825153351, 0.3759610056877136, 0.3638794720172882, 0.4363403916358948, 0.24463322758674622, 0.016175279393792152, 0.014034891501069069, 0.15634796023368835, 0.21940714120864868, 0.31649571657180786, 0.763617753982544, 0.8242884874343872, 0.5883968472480774, 0.06002708896994591, 0.021962294355034828, 0.0024008776526898146, 0.05870017036795616, 0.06495744735002518, 0.2546616494655609, 0.5586676001548767, 0.8368721604347229, 0.04912634938955307, 0.019776765257120132, 0.013286319561302662, 0.0005793613381683826, 0.3900282084941864, 0.26565492153167725, 0.5160230994224548, 0.9372599720954895, 0.9749610424041748, 0.402459055185318, 0.1461663395166397, 0.017872918397188187, 0.016474133357405663, 0.4194425046443939, 0.7804387211799622, 0.9433783292770386, 0.9895066022872925, 0.9787696599960327, 0.5724721550941467, 0.1809379756450653, 0.15044806897640228, 0.03902287036180496, 0.6065672636032104, 0.9183623790740967, 0.9981423020362854, 0.9925916790962219, 0.8051732182502747, 0.2920267581939697, 0.004329930990934372, 0.001743402099236846, 0.004750567954033613, 0.9734861254692078, 0.1374809741973877, 0.05401984229683876, 0.01837339624762535, 0.0163206048309803, 0.007249115966260433, 0.008688676171004772, 0.018351685255765915, 0.02164692059159279, 0.8242896199226379, 0.10578954964876175, 0.03890813887119293, 0.13291649520397186, 0.11650087684392929, 0.022387361153960228, 0.015191460028290749, 0.03175981715321541, 0.011223052628338337, 0.7934597134590149, 0.38556817173957825, 0.19115711748600006, 0.5173042416572571, 0.28223028779029846, 0.2483171820640564, 0.10793901234865189, 0.05800192803144455, 0.010297705419361591, 0.29882875084877014, 0.10554193705320358, 0.30510979890823364, 0.5615017414093018, 0.6499015688896179, 0.17074143886566162, 0.06733200699090958, 0.007534055970609188, 0.00368851819075644, 0.3178832232952118, 0.11489070951938629, 0.15339593589305878, 0.5811575651168823, 0.7124435305595398, 0.21938546001911163, 0.017117254436016083, 0.00091215455904603, 0.00016626961587462574, 0.7823334336280823, 0.5564435720443726, 0.7459773421287537, 0.9428247213363647, 0.906158447265625, 0.46177005767822266, 0.10572976619005203, 0.023506345227360725, 0.0010004211217164993, 0.7730580568313599, 0.4708198308944702, 0.9357206225395203, 0.988322377204895, 0.987572431564331, 0.4292345643043518, 0.15406490862369537, 0.05628373101353645, 0.011329490691423416, 0.15766359865665436, 0.8421526551246643, 0.9790674448013306, 0.9984972476959229, 0.9186737537384033, 0.22270335257053375, 0.004889292176812887, 0.0021661773789674044, 0.007955409586429596, 0.26999497413635254, 0.05651780962944031, 0.05046220123767853, 0.014368278905749321, 0.002416253322735429, 0.0007103332900442183, 0.003783422289416194, 0.003908967133611441, 0.006160776596516371, 0.5786407589912415, 0.771623969078064, 0.1652519553899765, 0.08877858519554138, 0.010129357688128948, 0.004066316410899162, 0.006653024815022945, 0.010777621529996395, 0.015371684916317463, 0.9239349961280823, 0.8420916795730591, 0.6621816158294678, 0.48380404710769653, 0.23091989755630493, 0.018160413950681686, 0.04436581954360008, 0.03655540943145752, 0.09200375527143478, 0.5385303497314453, 0.6273159980773926, 0.8137938380241394, 0.9626559615135193, 0.8859420418739319, 0.4924725592136383, 0.14467966556549072, 0.048656582832336426, 0.011090707033872604, 0.2822672724723816, 0.4796997904777527, 0.44503769278526306, 0.5951470136642456, 0.8819200992584229, 0.8180304765701294, 0.11254142224788666, 0.0019173943437635899, 0.00026608200278133154, 0.862504243850708, 0.4899362325668335, 0.5042241215705872, 0.7237051129341125, 0.9175408482551575, 0.8992620706558228, 0.17709442973136902, 0.026629246771335602, 0.0007309389184229076, 0.6920908689498901, 0.8351729512214661, 0.7759986519813538, 0.9384331107139587, 0.9568563103675842, 0.6665611863136292, 0.1466618925333023, 0.07675610482692719, 0.07741868495941162, 0.5778765082359314, 0.4013153910636902, 0.9383063912391663, 0.9883129000663757, 0.887401819229126, 0.046116285026073456, 0.0028072765562683344, 0.022630997002124786, 0.19556643068790436] + }, "objects": { "dtype": "uint64", "shape": [4, 8, 9], - "data": [1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 2, 2, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 3, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 4, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1] + "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 1, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0] }, - "count": 4 + "count": 2 }, { "id": "mutex_high_min4", - "method": "mutex_watershed", - "block": "aff_unit", "output_class": "unit", - "channels": [0, 1, 2], "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "threshold": 0.7, "min_size": 4, - "connectivity": 1, - "per_slice": false, + "affinities": { + "dtype": "float32", + "shape": [3, 4, 8, 9], + "data": [0.006405787542462349, 0.09727895259857178, 0.16088104248046875, 0.01466405764222145, 0.005348739679902792, 0.0009836857207119465, 0.0008501893607899547, 0.0007184659480117261, 0.00361567921936512, 0.023111745715141296, 0.4801984429359436, 0.7475154995918274, 0.29652276635169983, 0.11163563281297684, 0.03805999830365181, 0.0008071252377703786, 0.0002087654429487884, 0.00037951424019411206, 0.1419934779405594, 0.685640811920166, 0.8455166816711426, 0.6160955429077148, 0.2896983325481415, 0.6411769390106201, 0.04796143248677254, 0.010922696441411972, 0.0009256632765755057, 0.10980672389268875, 0.39533811807632446, 0.5900510549545288, 0.7259947657585144, 0.892128050327301, 0.9393881559371948, 0.12829263508319855, 0.016024377197027206, 0.013484541326761246, 0.022845521569252014, 0.009178868494927883, 0.38717421889305115, 0.9383704662322998, 0.9533053040504456, 0.9247043132781982, 0.35003551840782166, 0.20298410952091217, 0.12133221328258514, 0.019167760387063026, 0.02153478004038334, 0.23183898627758026, 0.9496617913246155, 0.9965312480926514, 0.9739004373550415, 0.9196115136146545, 0.8520665168762207, 0.7459754347801208, 0.19148880243301392, 0.7444937229156494, 0.9323265552520752, 0.9360714554786682, 0.898582398891449, 0.9626148343086243, 0.8647717833518982, 0.8427475690841675, 0.6785611510276794, 0.7403771877288818, 0.978455126285553, 0.9520742297172546, 0.891749382019043, 0.38169166445732117, 0.2778847813606262, 0.02232198230922222, 0.009397313930094242, 0.031109606847167015, 0.8451650142669678, 0.10122910141944885, 0.10893440991640091, 0.07132386416196823, 0.09226083010435104, 0.006728814914822578, 0.005487055517733097, 0.020825456827878952, 0.030349858105182648, 0.4935806691646576, 0.26552248001098633, 0.16372177004814148, 0.29161378741264343, 0.07623057067394257, 0.02803158573806286, 0.04092201218008995, 0.00999289471656084, 0.003432896686717868, 0.31116461753845215, 0.4003812372684479, 0.4084051549434662, 0.26420125365257263, 0.5042476654052734, 0.5529907941818237, 0.31194642186164856, 0.015944773331284523, 0.0103612607344985, 0.40324026346206665, 0.12452642619609833, 0.342197984457016, 0.6234222650527954, 0.7146673798561096, 0.6825367212295532, 0.13847388327121735, 0.023022405803203583, 0.001214761519804597, 0.1197289451956749, 0.039545439183712006, 0.2656973898410797, 0.7078984379768372, 0.8934940695762634, 0.3431282937526703, 0.02259020134806633, 0.005757425911724567, 0.0008348029223270714, 0.12116753309965134, 0.17550402879714966, 0.5276204347610474, 0.9808399081230164, 0.9827712774276733, 0.442697674036026, 0.2518978714942932, 0.07100124657154083, 0.026931291446089745, 0.7023043632507324, 0.8918875455856323, 0.9407371282577515, 0.9891384840011597, 0.9804701805114746, 0.744357705116272, 0.03922814503312111, 0.1006072610616684, 0.03492124006152153, 0.19476120173931122, 0.8906826972961426, 0.9940574765205383, 0.9975727200508118, 0.9124335050582886, 0.15608137845993042, 0.009714008308947086, 0.0005685868673026562, 0.0028838152065873146, 0.9684820175170898, 0.14863833785057068, 0.02140953578054905, 0.02522405982017517, 0.009544783271849155, 0.0035504119005054235, 0.0060817464254796505, 0.014942399226129055, 0.016097957268357277, 0.9338895082473755, 0.2003743052482605, 0.07766573131084442, 0.09010124951601028, 0.15433838963508606, 0.028244270011782646, 0.015032581053674221, 0.022749900817871094, 0.007086070254445076, 0.713589608669281, 0.4698563814163208, 0.24384739995002747, 0.5995345711708069, 0.2422187477350235, 0.1000564694404602, 0.06074699014425278, 0.0553671233355999, 0.019963808357715607, 0.32123124599456787, 0.19557100534439087, 0.1437099725008011, 0.7205166220664978, 0.6229304671287537, 0.20681019127368927, 0.06795535236597061, 0.018874960020184517, 0.003169267438352108, 0.29055291414260864, 0.22719217836856842, 0.5093474388122559, 0.46589040756225586, 0.6188870072364807, 0.2447742521762848, 0.02386835403740406, 0.0007393567357212305, 8.486895239911973e-05, 0.49906954169273376, 0.6174333691596985, 0.4286477863788605, 0.9189798831939697, 0.9521164298057556, 0.5627703070640564, 0.053999677300453186, 0.009448239579796791, 0.0013868428068235517, 0.8636879324913025, 0.5927143692970276, 0.7062453031539917, 0.991646409034729, 0.9522122144699097, 0.698702335357666, 0.14986637234687805, 0.032975345849990845, 0.012944228947162628, 0.24933764338493347, 0.5741854906082153, 0.9455171227455139, 0.9977059364318848, 0.9338324666023254, 0.24771615862846375, 0.003724754089489579, 0.0009586532833054662, 0.002418813295662403, 0.16659937798976898, 0.14725081622600555, 0.034873832017183304, 0.00478315306827426, 0.007050607353448868, 0.0012430199421942234, 0.0014806532999500632, 0.0017984578153118491, 0.007145723793655634, 0.5294622778892517, 0.7156361937522888, 0.12347240746021271, 0.07190761715173721, 0.029341809451580048, 0.0037194425240159035, 0.0024966285564005375, 0.016582781448960304, 0.008220940828323364, 0.8644982576370239, 0.774003267288208, 0.927916944026947, 0.6397309303283691, 0.14958657324314117, 0.01809884048998356, 0.015695104375481606, 0.06346869468688965, 0.02550654485821724, 0.7866106033325195, 0.7283308506011963, 0.9321506023406982, 0.9482742547988892, 0.902815580368042, 0.36903971433639526, 0.07919389754533768, 0.014410452917218208, 0.0016151153249666095, 0.15568168461322784, 0.6373230814933777, 0.8571060299873352, 0.7333729863166809, 0.746910035610199, 0.7672332525253296, 0.15452739596366882, 0.0009892996167764068, 0.00013574546028394252, 0.8373446464538574, 0.5875785946846008, 0.6509594321250916, 0.659740686416626, 0.8974853754043579, 0.8198027014732361, 0.21425434947013855, 0.016756094992160797, 0.001395352534018457, 0.9202431440353394, 0.8624597787857056, 0.9555229544639587, 0.9267820715904236, 0.9297482371330261, 0.8606886267662048, 0.35940679907798767, 0.08139093220233917, 0.1542719602584839, 0.1797739416360855, 0.5315520763397217, 0.9590945243835449, 0.9896495938301086, 0.9228262305259705, 0.10271875560283661, 0.002909652190282941, 0.020892489701509476, 0.37895193696022034, 0.036463551223278046, 0.27709120512008667, 0.23738320171833038, 0.024375230073928833, 0.014955307357013226, 0.004074228461831808, 0.0009376180241815746, 0.00029187617474235594, 0.0010817673755809665, 0.017200732603669167, 0.4444396197795868, 0.7992346882820129, 0.1523294299840927, 0.02471093088388443, 0.011450223624706268, 0.002722658682614565, 0.00016372262325603515, 0.0014774196315556765, 0.47880834341049194, 0.8638392686843872, 0.8548949956893921, 0.3515254557132721, 0.5757929086685181, 0.4932696521282196, 0.07410646229982376, 0.0048089949414134026, 0.00622129812836647, 0.3016401529312134, 0.5596623420715332, 0.3977149426937103, 0.2782095670700073, 0.8331773281097412, 0.9201893210411072, 0.3025350272655487, 0.05041160434484482, 0.010621345601975918, 0.015816206112504005, 0.017386458814144135, 0.0777549296617508, 0.8212324976921082, 0.9859774708747864, 0.748892068862915, 0.4023588001728058, 0.20803196728229523, 0.09152422845363617, 0.02683401107788086, 0.018570035696029663, 0.09454040229320526, 0.9009758830070496, 0.9955010414123535, 0.9845141768455505, 0.7415720224380493, 0.9277499914169312, 0.4548429548740387, 0.29804039001464844, 0.6117668747901917, 0.7855589985847473, 0.9545477032661438, 0.958476185798645, 0.9272050857543945, 0.8883187770843506, 0.858760416507721, 0.8754853010177612, 0.3832662105560303, 0.9524270296096802, 0.9616305828094482, 0.8907509446144104, 0.3620733916759491, 0.10983877629041672, 0.04657716676592827, 0.014462055638432503, 0.02250339463353157, 0.7154629230499268, 0.10569366812705994, 0.14334894716739655, 0.08471138775348663, 0.014225919730961323, 0.00822182185947895, 0.004308593459427357, 0.03918813169002533, 0.03596852719783783, 0.6235819458961487, 0.40530654788017273, 0.282954603433609, 0.12767809629440308, 0.08226874470710754, 0.06275758892297745, 0.03349435329437256, 0.0038431594148278236, 0.01502765342593193, 0.17439816892147064, 0.11114143580198288, 0.5317608714103699, 0.6187898516654968, 0.44940507411956787, 0.4582734704017639, 0.04580692946910858, 0.03119811974465847, 0.005379194859415293, 0.16011156141757965, 0.27635979652404785, 0.2144898921251297, 0.805109441280365, 0.7985683679580688, 0.38571861386299133, 0.06431533396244049, 0.025711029767990112, 0.0037043134216219187, 0.08650750666856766, 0.09820803999900818, 0.1143355742096901, 0.6504985690116882, 0.9214207530021667, 0.3625200092792511, 0.021512825042009354, 0.003598347771912813, 0.0007117267232388258, 0.40182918310165405, 0.29010674357414246, 0.6876466870307922, 0.9573358297348022, 0.980923593044281, 0.5157013535499573, 0.10556859523057938, 0.06603454053401947, 0.010391706600785255, 0.31896814703941345, 0.8630676865577698, 0.9674144983291626, 0.9894375801086426, 0.942147433757782, 0.5651320815086365, 0.23744112253189087, 0.0765782818198204, 0.037936434149742126, 0.24656705558300018, 0.9755046963691711, 0.987320065498352, 0.9973815679550171, 0.9733712077140808, 0.15568718314170837, 0.0014592820079997182, 0.0007763439207337797, 0.003524448722600937, 0.9712328910827637, 0.3113802671432495, 0.04120058938860893, 0.029378242790699005, 0.009301147423684597, 0.01962519809603691, 0.006872847210615873, 0.03432290628552437, 0.02574305236339569, 0.9438517093658447, 0.3803180158138275, 0.016599861904978752, 0.046117398887872696, 0.08080612123012543, 0.01782093569636345, 0.010331135243177414, 0.015400949865579605, 0.016450047492980957, 0.7294918298721313, 0.59328293800354, 0.15634390711784363, 0.5180606842041016, 0.41202107071876526, 0.21463340520858765, 0.022310873493552208, 0.07038745284080505, 0.007681288756430149, 0.10690692067146301, 0.18599335849285126, 0.4049161672592163, 0.7779960036277771, 0.6279895901679993, 0.3181595206260681, 0.07276221364736557, 0.006887141149491072, 0.0009561369079165161, 0.3169105350971222, 0.2076488882303238, 0.4842545986175537, 0.6714500784873962, 0.6789632439613342, 0.3211117088794708, 0.0303569994866848, 0.0003437900741118938, 0.000224252202315256, 0.7955291271209717, 0.6867148280143738, 0.7030497789382935, 0.6080354452133179, 0.9137871265411377, 0.40626654028892517, 0.08090484887361526, 0.005014144349843264, 0.0008612490491941571, 0.43387120962142944, 0.78774493932724, 0.8998064398765564, 0.9974437952041626, 0.9735685586929321, 0.9130439758300781, 0.10426246374845505, 0.07976429164409637, 0.006124634295701981, 0.22978587448596954, 0.6298061609268188, 0.9497805237770081, 0.9979004859924316, 0.9811471104621887, 0.11227890849113464, 0.009679608978331089, 0.0015625111991539598, 0.005984739866107702, 0.3933556377887726, 0.07433469593524933, 0.073813296854496, 0.025041161105036736, 0.003479322884231806, 0.0027101568412035704, 0.003297708462923765, 0.0036330465227365494, 0.005828320048749447, 0.8169421553611755, 0.3659225404262543, 0.23431475460529327, 0.06277850270271301, 0.011350726708769798, 0.002585472073405981, 0.004223544616252184, 0.0055309501476585865, 0.011714592576026917, 0.6990140080451965, 0.7143499851226807, 0.8165850043296814, 0.7749465107917786, 0.18483403325080872, 0.01825827546417713, 0.019674180075526237, 0.06838100403547287, 0.08795469254255295, 0.7525714635848999, 0.7650346755981445, 0.8397188782691956, 0.9127547144889832, 0.7892559766769409, 0.2669185400009155, 0.08987037092447281, 0.03852618485689163, 0.003076256485655904, 0.21000468730926514, 0.449381023645401, 0.5557305216789246, 0.7972224354743958, 0.9337754249572754, 0.8973298668861389, 0.125515416264534, 0.0030785175040364265, 0.000563664420042187, 0.7774319648742676, 0.8161802291870117, 0.7318100929260254, 0.6054673790931702, 0.8640800714492798, 0.8725725412368774, 0.4066160321235657, 0.008981174789369106, 0.001729608979076147, 0.5481672883033752, 0.7885011434555054, 0.7658112645149231, 0.935683012008667, 0.9413362741470337, 0.4633576571941376, 0.29008156061172485, 0.07462448626756668, 0.04937174171209335, 0.3021053969860077, 0.4201965630054474, 0.9003274440765381, 0.9926263689994812, 0.9480562210083008, 0.042369596660137177, 0.005779182072728872, 0.008414193987846375, 0.24217785894870758, 0.0155562749132514, 0.09378772974014282, 0.16245222091674805, 0.03054456226527691, 0.002467889804393053, 0.004624940454959869, 0.0005982423317618668, 0.0003144068468827754, 0.000854213023558259, 0.0270580742508173, 0.14774569869041443, 0.6388610601425171, 0.240841343998909, 0.05799445882439613, 0.013676602393388748, 0.0007921734941191971, 0.000643699080683291, 0.001191712450236082, 0.13431710004806519, 0.8179196119308472, 0.6153555512428284, 0.35775843262672424, 0.5316541194915771, 0.5420814752578735, 0.06136036664247513, 0.003946713637560606, 0.003959944471716881, 0.13619516789913177, 0.40717384219169617, 0.4833129942417145, 0.3982491195201874, 0.7812288403511047, 0.9762170314788818, 0.13124923408031464, 0.056378304958343506, 0.02727123349905014, 0.02908400632441044, 0.03789268434047699, 0.18035878241062164, 0.7356791496276855, 0.9755395650863647, 0.8796552419662476, 0.32139745354652405, 0.25075605511665344, 0.054308950901031494, 0.011015450581908226, 0.0145757170394063, 0.17323996126651764, 0.9549662470817566, 0.9900804758071899, 0.9780991673469543, 0.904492974281311, 0.8705196380615234, 0.7373524308204651, 0.12912921607494354, 0.42098432779312134, 0.7970767617225647, 0.9468807578086853, 0.8865792155265808, 0.8455470204353333, 0.8828883171081543, 0.8131395578384399, 0.9220535755157471, 0.6693574786186218, 0.987352192401886, 0.965021550655365, 0.9445539712905884, 0.1642608493566513, 0.06971129775047302, 0.03678398206830025, 0.007050120271742344, 0.01761852763593197, 0.6688684821128845, 0.2852058410644531, 0.0315309576690197, 0.113181933760643, 0.018022330477833748, 0.027434615418314934, 0.01194030325859785, 0.01684282347559929, 0.07791317999362946, 0.22980593144893646, 0.35293638706207275, 0.25983723998069763, 0.07790030539035797, 0.34559959173202515, 0.08268477767705917, 0.021001406013965607, 0.013827490620315075, 0.002802387112751603, 0.37692463397979736, 0.35001325607299805, 0.4386589825153351, 0.3759610056877136, 0.3638794720172882, 0.4363403916358948, 0.24463322758674622, 0.016175279393792152, 0.014034891501069069, 0.15634796023368835, 0.21940714120864868, 0.31649571657180786, 0.763617753982544, 0.8242884874343872, 0.5883968472480774, 0.06002708896994591, 0.021962294355034828, 0.0024008776526898146, 0.05870017036795616, 0.06495744735002518, 0.2546616494655609, 0.5586676001548767, 0.8368721604347229, 0.04912634938955307, 0.019776765257120132, 0.013286319561302662, 0.0005793613381683826, 0.3900282084941864, 0.26565492153167725, 0.5160230994224548, 0.9372599720954895, 0.9749610424041748, 0.402459055185318, 0.1461663395166397, 0.017872918397188187, 0.016474133357405663, 0.4194425046443939, 0.7804387211799622, 0.9433783292770386, 0.9895066022872925, 0.9787696599960327, 0.5724721550941467, 0.1809379756450653, 0.15044806897640228, 0.03902287036180496, 0.6065672636032104, 0.9183623790740967, 0.9981423020362854, 0.9925916790962219, 0.8051732182502747, 0.2920267581939697, 0.004329930990934372, 0.001743402099236846, 0.004750567954033613, 0.9734861254692078, 0.1374809741973877, 0.05401984229683876, 0.01837339624762535, 0.0163206048309803, 0.007249115966260433, 0.008688676171004772, 0.018351685255765915, 0.02164692059159279, 0.8242896199226379, 0.10578954964876175, 0.03890813887119293, 0.13291649520397186, 0.11650087684392929, 0.022387361153960228, 0.015191460028290749, 0.03175981715321541, 0.011223052628338337, 0.7934597134590149, 0.38556817173957825, 0.19115711748600006, 0.5173042416572571, 0.28223028779029846, 0.2483171820640564, 0.10793901234865189, 0.05800192803144455, 0.010297705419361591, 0.29882875084877014, 0.10554193705320358, 0.30510979890823364, 0.5615017414093018, 0.6499015688896179, 0.17074143886566162, 0.06733200699090958, 0.007534055970609188, 0.00368851819075644, 0.3178832232952118, 0.11489070951938629, 0.15339593589305878, 0.5811575651168823, 0.7124435305595398, 0.21938546001911163, 0.017117254436016083, 0.00091215455904603, 0.00016626961587462574, 0.7823334336280823, 0.5564435720443726, 0.7459773421287537, 0.9428247213363647, 0.906158447265625, 0.46177005767822266, 0.10572976619005203, 0.023506345227360725, 0.0010004211217164993, 0.7730580568313599, 0.4708198308944702, 0.9357206225395203, 0.988322377204895, 0.987572431564331, 0.4292345643043518, 0.15406490862369537, 0.05628373101353645, 0.011329490691423416, 0.15766359865665436, 0.8421526551246643, 0.9790674448013306, 0.9984972476959229, 0.9186737537384033, 0.22270335257053375, 0.004889292176812887, 0.0021661773789674044, 0.007955409586429596, 0.26999497413635254, 0.05651780962944031, 0.05046220123767853, 0.014368278905749321, 0.002416253322735429, 0.0007103332900442183, 0.003783422289416194, 0.003908967133611441, 0.006160776596516371, 0.5786407589912415, 0.771623969078064, 0.1652519553899765, 0.08877858519554138, 0.010129357688128948, 0.004066316410899162, 0.006653024815022945, 0.010777621529996395, 0.015371684916317463, 0.9239349961280823, 0.8420916795730591, 0.6621816158294678, 0.48380404710769653, 0.23091989755630493, 0.018160413950681686, 0.04436581954360008, 0.03655540943145752, 0.09200375527143478, 0.5385303497314453, 0.6273159980773926, 0.8137938380241394, 0.9626559615135193, 0.8859420418739319, 0.4924725592136383, 0.14467966556549072, 0.048656582832336426, 0.011090707033872604, 0.2822672724723816, 0.4796997904777527, 0.44503769278526306, 0.5951470136642456, 0.8819200992584229, 0.8180304765701294, 0.11254142224788666, 0.0019173943437635899, 0.00026608200278133154, 0.862504243850708, 0.4899362325668335, 0.5042241215705872, 0.7237051129341125, 0.9175408482551575, 0.8992620706558228, 0.17709442973136902, 0.026629246771335602, 0.0007309389184229076, 0.6920908689498901, 0.8351729512214661, 0.7759986519813538, 0.9384331107139587, 0.9568563103675842, 0.6665611863136292, 0.1466618925333023, 0.07675610482692719, 0.07741868495941162, 0.5778765082359314, 0.4013153910636902, 0.9383063912391663, 0.9883129000663757, 0.887401819229126, 0.046116285026073456, 0.0028072765562683344, 0.022630997002124786, 0.19556643068790436] + }, "objects": { "dtype": "uint64", "shape": [4, 8, 9], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 2, 2, 1, 1, 1, 0, 0, 2, 2, 2, 2, 1, 1, 1, 0, 0, 2, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2, 2, 0, 1, 1, 1, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0] + "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 2, 3, 3, 3, 0, 0, 0, 0, 0, 2, 3, 1, 1, 0, 0, 0, 4, 4, 0, 1, 1, 1, 1, 0, 0, 0, 4, 4, 1, 1, 1, 0, 0, 0, 0, 4, 1, 1, 1, 0, 0, 0, 0] }, - "count": 2 + "count": 4 }, { "id": "mutex_classified_min3", - "method": "mutex_watershed", - "block": "aff_unit", "output_class": null, - "channels": [0, 1, 2], "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "threshold": 0.5, "min_size": 3, - "connectivity": 1, - "per_slice": false, + "affinities": { + "dtype": "float32", + "shape": [3, 4, 8, 9], + "data": [0.006405787542462349, 0.09727895259857178, 0.16088104248046875, 0.01466405764222145, 0.005348739679902792, 0.0009836857207119465, 0.0008501893607899547, 0.0007184659480117261, 0.00361567921936512, 0.023111745715141296, 0.4801984429359436, 0.7475154995918274, 0.29652276635169983, 0.11163563281297684, 0.03805999830365181, 0.0008071252377703786, 0.0002087654429487884, 0.00037951424019411206, 0.1419934779405594, 0.685640811920166, 0.8455166816711426, 0.6160955429077148, 0.2896983325481415, 0.6411769390106201, 0.04796143248677254, 0.010922696441411972, 0.0009256632765755057, 0.10980672389268875, 0.39533811807632446, 0.5900510549545288, 0.7259947657585144, 0.892128050327301, 0.9393881559371948, 0.12829263508319855, 0.016024377197027206, 0.013484541326761246, 0.022845521569252014, 0.009178868494927883, 0.38717421889305115, 0.9383704662322998, 0.9533053040504456, 0.9247043132781982, 0.35003551840782166, 0.20298410952091217, 0.12133221328258514, 0.019167760387063026, 0.02153478004038334, 0.23183898627758026, 0.9496617913246155, 0.9965312480926514, 0.9739004373550415, 0.9196115136146545, 0.8520665168762207, 0.7459754347801208, 0.19148880243301392, 0.7444937229156494, 0.9323265552520752, 0.9360714554786682, 0.898582398891449, 0.9626148343086243, 0.8647717833518982, 0.8427475690841675, 0.6785611510276794, 0.7403771877288818, 0.978455126285553, 0.9520742297172546, 0.891749382019043, 0.38169166445732117, 0.2778847813606262, 0.02232198230922222, 0.009397313930094242, 0.031109606847167015, 0.8451650142669678, 0.10122910141944885, 0.10893440991640091, 0.07132386416196823, 0.09226083010435104, 0.006728814914822578, 0.005487055517733097, 0.020825456827878952, 0.030349858105182648, 0.4935806691646576, 0.26552248001098633, 0.16372177004814148, 0.29161378741264343, 0.07623057067394257, 0.02803158573806286, 0.04092201218008995, 0.00999289471656084, 0.003432896686717868, 0.31116461753845215, 0.4003812372684479, 0.4084051549434662, 0.26420125365257263, 0.5042476654052734, 0.5529907941818237, 0.31194642186164856, 0.015944773331284523, 0.0103612607344985, 0.40324026346206665, 0.12452642619609833, 0.342197984457016, 0.6234222650527954, 0.7146673798561096, 0.6825367212295532, 0.13847388327121735, 0.023022405803203583, 0.001214761519804597, 0.1197289451956749, 0.039545439183712006, 0.2656973898410797, 0.7078984379768372, 0.8934940695762634, 0.3431282937526703, 0.02259020134806633, 0.005757425911724567, 0.0008348029223270714, 0.12116753309965134, 0.17550402879714966, 0.5276204347610474, 0.9808399081230164, 0.9827712774276733, 0.442697674036026, 0.2518978714942932, 0.07100124657154083, 0.026931291446089745, 0.7023043632507324, 0.8918875455856323, 0.9407371282577515, 0.9891384840011597, 0.9804701805114746, 0.744357705116272, 0.03922814503312111, 0.1006072610616684, 0.03492124006152153, 0.19476120173931122, 0.8906826972961426, 0.9940574765205383, 0.9975727200508118, 0.9124335050582886, 0.15608137845993042, 0.009714008308947086, 0.0005685868673026562, 0.0028838152065873146, 0.9684820175170898, 0.14863833785057068, 0.02140953578054905, 0.02522405982017517, 0.009544783271849155, 0.0035504119005054235, 0.0060817464254796505, 0.014942399226129055, 0.016097957268357277, 0.9338895082473755, 0.2003743052482605, 0.07766573131084442, 0.09010124951601028, 0.15433838963508606, 0.028244270011782646, 0.015032581053674221, 0.022749900817871094, 0.007086070254445076, 0.713589608669281, 0.4698563814163208, 0.24384739995002747, 0.5995345711708069, 0.2422187477350235, 0.1000564694404602, 0.06074699014425278, 0.0553671233355999, 0.019963808357715607, 0.32123124599456787, 0.19557100534439087, 0.1437099725008011, 0.7205166220664978, 0.6229304671287537, 0.20681019127368927, 0.06795535236597061, 0.018874960020184517, 0.003169267438352108, 0.29055291414260864, 0.22719217836856842, 0.5093474388122559, 0.46589040756225586, 0.6188870072364807, 0.2447742521762848, 0.02386835403740406, 0.0007393567357212305, 8.486895239911973e-05, 0.49906954169273376, 0.6174333691596985, 0.4286477863788605, 0.9189798831939697, 0.9521164298057556, 0.5627703070640564, 0.053999677300453186, 0.009448239579796791, 0.0013868428068235517, 0.8636879324913025, 0.5927143692970276, 0.7062453031539917, 0.991646409034729, 0.9522122144699097, 0.698702335357666, 0.14986637234687805, 0.032975345849990845, 0.012944228947162628, 0.24933764338493347, 0.5741854906082153, 0.9455171227455139, 0.9977059364318848, 0.9338324666023254, 0.24771615862846375, 0.003724754089489579, 0.0009586532833054662, 0.002418813295662403, 0.16659937798976898, 0.14725081622600555, 0.034873832017183304, 0.00478315306827426, 0.007050607353448868, 0.0012430199421942234, 0.0014806532999500632, 0.0017984578153118491, 0.007145723793655634, 0.5294622778892517, 0.7156361937522888, 0.12347240746021271, 0.07190761715173721, 0.029341809451580048, 0.0037194425240159035, 0.0024966285564005375, 0.016582781448960304, 0.008220940828323364, 0.8644982576370239, 0.774003267288208, 0.927916944026947, 0.6397309303283691, 0.14958657324314117, 0.01809884048998356, 0.015695104375481606, 0.06346869468688965, 0.02550654485821724, 0.7866106033325195, 0.7283308506011963, 0.9321506023406982, 0.9482742547988892, 0.902815580368042, 0.36903971433639526, 0.07919389754533768, 0.014410452917218208, 0.0016151153249666095, 0.15568168461322784, 0.6373230814933777, 0.8571060299873352, 0.7333729863166809, 0.746910035610199, 0.7672332525253296, 0.15452739596366882, 0.0009892996167764068, 0.00013574546028394252, 0.8373446464538574, 0.5875785946846008, 0.6509594321250916, 0.659740686416626, 0.8974853754043579, 0.8198027014732361, 0.21425434947013855, 0.016756094992160797, 0.001395352534018457, 0.9202431440353394, 0.8624597787857056, 0.9555229544639587, 0.9267820715904236, 0.9297482371330261, 0.8606886267662048, 0.35940679907798767, 0.08139093220233917, 0.1542719602584839, 0.1797739416360855, 0.5315520763397217, 0.9590945243835449, 0.9896495938301086, 0.9228262305259705, 0.10271875560283661, 0.002909652190282941, 0.020892489701509476, 0.37895193696022034, 0.036463551223278046, 0.27709120512008667, 0.23738320171833038, 0.024375230073928833, 0.014955307357013226, 0.004074228461831808, 0.0009376180241815746, 0.00029187617474235594, 0.0010817673755809665, 0.017200732603669167, 0.4444396197795868, 0.7992346882820129, 0.1523294299840927, 0.02471093088388443, 0.011450223624706268, 0.002722658682614565, 0.00016372262325603515, 0.0014774196315556765, 0.47880834341049194, 0.8638392686843872, 0.8548949956893921, 0.3515254557132721, 0.5757929086685181, 0.4932696521282196, 0.07410646229982376, 0.0048089949414134026, 0.00622129812836647, 0.3016401529312134, 0.5596623420715332, 0.3977149426937103, 0.2782095670700073, 0.8331773281097412, 0.9201893210411072, 0.3025350272655487, 0.05041160434484482, 0.010621345601975918, 0.015816206112504005, 0.017386458814144135, 0.0777549296617508, 0.8212324976921082, 0.9859774708747864, 0.748892068862915, 0.4023588001728058, 0.20803196728229523, 0.09152422845363617, 0.02683401107788086, 0.018570035696029663, 0.09454040229320526, 0.9009758830070496, 0.9955010414123535, 0.9845141768455505, 0.7415720224380493, 0.9277499914169312, 0.4548429548740387, 0.29804039001464844, 0.6117668747901917, 0.7855589985847473, 0.9545477032661438, 0.958476185798645, 0.9272050857543945, 0.8883187770843506, 0.858760416507721, 0.8754853010177612, 0.3832662105560303, 0.9524270296096802, 0.9616305828094482, 0.8907509446144104, 0.3620733916759491, 0.10983877629041672, 0.04657716676592827, 0.014462055638432503, 0.02250339463353157, 0.7154629230499268, 0.10569366812705994, 0.14334894716739655, 0.08471138775348663, 0.014225919730961323, 0.00822182185947895, 0.004308593459427357, 0.03918813169002533, 0.03596852719783783, 0.6235819458961487, 0.40530654788017273, 0.282954603433609, 0.12767809629440308, 0.08226874470710754, 0.06275758892297745, 0.03349435329437256, 0.0038431594148278236, 0.01502765342593193, 0.17439816892147064, 0.11114143580198288, 0.5317608714103699, 0.6187898516654968, 0.44940507411956787, 0.4582734704017639, 0.04580692946910858, 0.03119811974465847, 0.005379194859415293, 0.16011156141757965, 0.27635979652404785, 0.2144898921251297, 0.805109441280365, 0.7985683679580688, 0.38571861386299133, 0.06431533396244049, 0.025711029767990112, 0.0037043134216219187, 0.08650750666856766, 0.09820803999900818, 0.1143355742096901, 0.6504985690116882, 0.9214207530021667, 0.3625200092792511, 0.021512825042009354, 0.003598347771912813, 0.0007117267232388258, 0.40182918310165405, 0.29010674357414246, 0.6876466870307922, 0.9573358297348022, 0.980923593044281, 0.5157013535499573, 0.10556859523057938, 0.06603454053401947, 0.010391706600785255, 0.31896814703941345, 0.8630676865577698, 0.9674144983291626, 0.9894375801086426, 0.942147433757782, 0.5651320815086365, 0.23744112253189087, 0.0765782818198204, 0.037936434149742126, 0.24656705558300018, 0.9755046963691711, 0.987320065498352, 0.9973815679550171, 0.9733712077140808, 0.15568718314170837, 0.0014592820079997182, 0.0007763439207337797, 0.003524448722600937, 0.9712328910827637, 0.3113802671432495, 0.04120058938860893, 0.029378242790699005, 0.009301147423684597, 0.01962519809603691, 0.006872847210615873, 0.03432290628552437, 0.02574305236339569, 0.9438517093658447, 0.3803180158138275, 0.016599861904978752, 0.046117398887872696, 0.08080612123012543, 0.01782093569636345, 0.010331135243177414, 0.015400949865579605, 0.016450047492980957, 0.7294918298721313, 0.59328293800354, 0.15634390711784363, 0.5180606842041016, 0.41202107071876526, 0.21463340520858765, 0.022310873493552208, 0.07038745284080505, 0.007681288756430149, 0.10690692067146301, 0.18599335849285126, 0.4049161672592163, 0.7779960036277771, 0.6279895901679993, 0.3181595206260681, 0.07276221364736557, 0.006887141149491072, 0.0009561369079165161, 0.3169105350971222, 0.2076488882303238, 0.4842545986175537, 0.6714500784873962, 0.6789632439613342, 0.3211117088794708, 0.0303569994866848, 0.0003437900741118938, 0.000224252202315256, 0.7955291271209717, 0.6867148280143738, 0.7030497789382935, 0.6080354452133179, 0.9137871265411377, 0.40626654028892517, 0.08090484887361526, 0.005014144349843264, 0.0008612490491941571, 0.43387120962142944, 0.78774493932724, 0.8998064398765564, 0.9974437952041626, 0.9735685586929321, 0.9130439758300781, 0.10426246374845505, 0.07976429164409637, 0.006124634295701981, 0.22978587448596954, 0.6298061609268188, 0.9497805237770081, 0.9979004859924316, 0.9811471104621887, 0.11227890849113464, 0.009679608978331089, 0.0015625111991539598, 0.005984739866107702, 0.3933556377887726, 0.07433469593524933, 0.073813296854496, 0.025041161105036736, 0.003479322884231806, 0.0027101568412035704, 0.003297708462923765, 0.0036330465227365494, 0.005828320048749447, 0.8169421553611755, 0.3659225404262543, 0.23431475460529327, 0.06277850270271301, 0.011350726708769798, 0.002585472073405981, 0.004223544616252184, 0.0055309501476585865, 0.011714592576026917, 0.6990140080451965, 0.7143499851226807, 0.8165850043296814, 0.7749465107917786, 0.18483403325080872, 0.01825827546417713, 0.019674180075526237, 0.06838100403547287, 0.08795469254255295, 0.7525714635848999, 0.7650346755981445, 0.8397188782691956, 0.9127547144889832, 0.7892559766769409, 0.2669185400009155, 0.08987037092447281, 0.03852618485689163, 0.003076256485655904, 0.21000468730926514, 0.449381023645401, 0.5557305216789246, 0.7972224354743958, 0.9337754249572754, 0.8973298668861389, 0.125515416264534, 0.0030785175040364265, 0.000563664420042187, 0.7774319648742676, 0.8161802291870117, 0.7318100929260254, 0.6054673790931702, 0.8640800714492798, 0.8725725412368774, 0.4066160321235657, 0.008981174789369106, 0.001729608979076147, 0.5481672883033752, 0.7885011434555054, 0.7658112645149231, 0.935683012008667, 0.9413362741470337, 0.4633576571941376, 0.29008156061172485, 0.07462448626756668, 0.04937174171209335, 0.3021053969860077, 0.4201965630054474, 0.9003274440765381, 0.9926263689994812, 0.9480562210083008, 0.042369596660137177, 0.005779182072728872, 0.008414193987846375, 0.24217785894870758, 0.0155562749132514, 0.09378772974014282, 0.16245222091674805, 0.03054456226527691, 0.002467889804393053, 0.004624940454959869, 0.0005982423317618668, 0.0003144068468827754, 0.000854213023558259, 0.0270580742508173, 0.14774569869041443, 0.6388610601425171, 0.240841343998909, 0.05799445882439613, 0.013676602393388748, 0.0007921734941191971, 0.000643699080683291, 0.001191712450236082, 0.13431710004806519, 0.8179196119308472, 0.6153555512428284, 0.35775843262672424, 0.5316541194915771, 0.5420814752578735, 0.06136036664247513, 0.003946713637560606, 0.003959944471716881, 0.13619516789913177, 0.40717384219169617, 0.4833129942417145, 0.3982491195201874, 0.7812288403511047, 0.9762170314788818, 0.13124923408031464, 0.056378304958343506, 0.02727123349905014, 0.02908400632441044, 0.03789268434047699, 0.18035878241062164, 0.7356791496276855, 0.9755395650863647, 0.8796552419662476, 0.32139745354652405, 0.25075605511665344, 0.054308950901031494, 0.011015450581908226, 0.0145757170394063, 0.17323996126651764, 0.9549662470817566, 0.9900804758071899, 0.9780991673469543, 0.904492974281311, 0.8705196380615234, 0.7373524308204651, 0.12912921607494354, 0.42098432779312134, 0.7970767617225647, 0.9468807578086853, 0.8865792155265808, 0.8455470204353333, 0.8828883171081543, 0.8131395578384399, 0.9220535755157471, 0.6693574786186218, 0.987352192401886, 0.965021550655365, 0.9445539712905884, 0.1642608493566513, 0.06971129775047302, 0.03678398206830025, 0.007050120271742344, 0.01761852763593197, 0.6688684821128845, 0.2852058410644531, 0.0315309576690197, 0.113181933760643, 0.018022330477833748, 0.027434615418314934, 0.01194030325859785, 0.01684282347559929, 0.07791317999362946, 0.22980593144893646, 0.35293638706207275, 0.25983723998069763, 0.07790030539035797, 0.34559959173202515, 0.08268477767705917, 0.021001406013965607, 0.013827490620315075, 0.002802387112751603, 0.37692463397979736, 0.35001325607299805, 0.4386589825153351, 0.3759610056877136, 0.3638794720172882, 0.4363403916358948, 0.24463322758674622, 0.016175279393792152, 0.014034891501069069, 0.15634796023368835, 0.21940714120864868, 0.31649571657180786, 0.763617753982544, 0.8242884874343872, 0.5883968472480774, 0.06002708896994591, 0.021962294355034828, 0.0024008776526898146, 0.05870017036795616, 0.06495744735002518, 0.2546616494655609, 0.5586676001548767, 0.8368721604347229, 0.04912634938955307, 0.019776765257120132, 0.013286319561302662, 0.0005793613381683826, 0.3900282084941864, 0.26565492153167725, 0.5160230994224548, 0.9372599720954895, 0.9749610424041748, 0.402459055185318, 0.1461663395166397, 0.017872918397188187, 0.016474133357405663, 0.4194425046443939, 0.7804387211799622, 0.9433783292770386, 0.9895066022872925, 0.9787696599960327, 0.5724721550941467, 0.1809379756450653, 0.15044806897640228, 0.03902287036180496, 0.6065672636032104, 0.9183623790740967, 0.9981423020362854, 0.9925916790962219, 0.8051732182502747, 0.2920267581939697, 0.004329930990934372, 0.001743402099236846, 0.004750567954033613, 0.9734861254692078, 0.1374809741973877, 0.05401984229683876, 0.01837339624762535, 0.0163206048309803, 0.007249115966260433, 0.008688676171004772, 0.018351685255765915, 0.02164692059159279, 0.8242896199226379, 0.10578954964876175, 0.03890813887119293, 0.13291649520397186, 0.11650087684392929, 0.022387361153960228, 0.015191460028290749, 0.03175981715321541, 0.011223052628338337, 0.7934597134590149, 0.38556817173957825, 0.19115711748600006, 0.5173042416572571, 0.28223028779029846, 0.2483171820640564, 0.10793901234865189, 0.05800192803144455, 0.010297705419361591, 0.29882875084877014, 0.10554193705320358, 0.30510979890823364, 0.5615017414093018, 0.6499015688896179, 0.17074143886566162, 0.06733200699090958, 0.007534055970609188, 0.00368851819075644, 0.3178832232952118, 0.11489070951938629, 0.15339593589305878, 0.5811575651168823, 0.7124435305595398, 0.21938546001911163, 0.017117254436016083, 0.00091215455904603, 0.00016626961587462574, 0.7823334336280823, 0.5564435720443726, 0.7459773421287537, 0.9428247213363647, 0.906158447265625, 0.46177005767822266, 0.10572976619005203, 0.023506345227360725, 0.0010004211217164993, 0.7730580568313599, 0.4708198308944702, 0.9357206225395203, 0.988322377204895, 0.987572431564331, 0.4292345643043518, 0.15406490862369537, 0.05628373101353645, 0.011329490691423416, 0.15766359865665436, 0.8421526551246643, 0.9790674448013306, 0.9984972476959229, 0.9186737537384033, 0.22270335257053375, 0.004889292176812887, 0.0021661773789674044, 0.007955409586429596, 0.26999497413635254, 0.05651780962944031, 0.05046220123767853, 0.014368278905749321, 0.002416253322735429, 0.0007103332900442183, 0.003783422289416194, 0.003908967133611441, 0.006160776596516371, 0.5786407589912415, 0.771623969078064, 0.1652519553899765, 0.08877858519554138, 0.010129357688128948, 0.004066316410899162, 0.006653024815022945, 0.010777621529996395, 0.015371684916317463, 0.9239349961280823, 0.8420916795730591, 0.6621816158294678, 0.48380404710769653, 0.23091989755630493, 0.018160413950681686, 0.04436581954360008, 0.03655540943145752, 0.09200375527143478, 0.5385303497314453, 0.6273159980773926, 0.8137938380241394, 0.9626559615135193, 0.8859420418739319, 0.4924725592136383, 0.14467966556549072, 0.048656582832336426, 0.011090707033872604, 0.2822672724723816, 0.4796997904777527, 0.44503769278526306, 0.5951470136642456, 0.8819200992584229, 0.8180304765701294, 0.11254142224788666, 0.0019173943437635899, 0.00026608200278133154, 0.862504243850708, 0.4899362325668335, 0.5042241215705872, 0.7237051129341125, 0.9175408482551575, 0.8992620706558228, 0.17709442973136902, 0.026629246771335602, 0.0007309389184229076, 0.6920908689498901, 0.8351729512214661, 0.7759986519813538, 0.9384331107139587, 0.9568563103675842, 0.6665611863136292, 0.1466618925333023, 0.07675610482692719, 0.07741868495941162, 0.5778765082359314, 0.4013153910636902, 0.9383063912391663, 0.9883129000663757, 0.887401819229126, 0.046116285026073456, 0.0028072765562683344, 0.022630997002124786, 0.19556643068790436] + }, "objects": { "dtype": "uint64", "shape": [4, 8, 9], - "data": [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 0, 0, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 0, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 0, 0, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 2, 2, 2, 0, 0, 1, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, 0, 2, 2, 2, 2, 2, 1, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2] + "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0] }, - "count": 2 + "count": 1 }, { "id": "mutex_logits", - "method": "mutex_watershed", - "block": "aff_small", "output_class": "unbounded", - "channels": [0, 1, 2], "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "threshold": 0.5, "min_size": 0, - "connectivity": 1, - "per_slice": false, + "affinities": { + "dtype": "float32", + "shape": [3, 3, 5, 6], + "data": [0.8173035979270935, 0.6709539890289307, 0.5314658880233765, 0.6084961295127869, 0.580284595489502, 0.39640340209007263, 0.8138487339019775, 0.572124183177948, 0.30029675364494324, 0.39452648162841797, 0.6074596643447876, 0.6964578628540039, 0.9113277792930603, 0.5956977009773254, 0.48067590594291687, 0.48869192600250244, 0.6830602288246155, 0.8923346400260925, 0.8191739320755005, 0.7911310791969299, 0.8089491128921509, 0.8573397397994995, 0.931551456451416, 0.9605108499526978, 0.540343165397644, 0.6995229125022888, 0.8376197814941406, 0.9495235681533813, 0.9834397435188293, 0.9828463196754456, 0.8062157034873962, 0.4805664122104645, 0.24930432438850403, 0.14998309314250946, 0.30243417620658875, 0.27364030480384827, 0.7696471214294434, 0.38701876997947693, 0.3045375347137451, 0.3102498948574066, 0.41769421100616455, 0.4155409336090088, 0.8262365460395813, 0.6279906034469604, 0.5950109362602234, 0.524612545967102, 0.7424357533454895, 0.6911601424217224, 0.9068256616592407, 0.7956187725067139, 0.7641294598579407, 0.7723896503448486, 0.8902938961982727, 0.8995878100395203, 0.8448649048805237, 0.7977418303489685, 0.8189159631729126, 0.8688940405845642, 0.9213672280311584, 0.9301550388336182, 0.6071709394454956, 0.29985326528549194, 0.11862216144800186, 0.22468425333499908, 0.35070356726646423, 0.3602716326713562, 0.6548090577125549, 0.357278436422348, 0.13100528717041016, 0.19024153053760529, 0.4414389133453369, 0.5682706832885742, 0.7504817843437195, 0.49855589866638184, 0.2053276002407074, 0.49666181206703186, 0.4698314666748047, 0.6031646132469177, 0.8773792386054993, 0.8162015080451965, 0.6982517242431641, 0.5445089936256409, 0.6130658984184265, 0.6829502582550049, 0.8334912657737732, 0.7786041498184204, 0.8568989634513855, 0.8189595937728882, 0.6456342339515686, 0.6066650152206421, 0.8705799579620361, 0.5936280488967896, 0.5990898013114929, 0.5707679390907288, 0.699874997138977, 0.5258734226226807, 0.8237023949623108, 0.4830091893672943, 0.3440220057964325, 0.39106249809265137, 0.5272782444953918, 0.5401390790939331, 0.90681391954422, 0.6455284953117371, 0.5193508267402649, 0.5085607767105103, 0.6066353917121887, 0.8191872239112854, 0.8544543981552124, 0.8705684542655945, 0.8089056611061096, 0.8915902376174927, 0.9192348122596741, 0.9730367064476013, 0.5532479882240295, 0.6282135248184204, 0.8323439359664917, 0.9617546796798706, 0.9843360185623169, 0.9895327091217041, 0.7972518801689148, 0.43719586730003357, 0.19822420179843903, 0.23676268756389618, 0.24064379930496216, 0.25658270716667175, 0.7170876264572144, 0.3191055655479431, 0.24815791845321655, 0.2125205099582672, 0.6145243048667908, 0.5946095585823059, 0.8729150295257568, 0.6129506230354309, 0.42015886306762695, 0.564541757106781, 0.6118093132972717, 0.7492960691452026, 0.8672401905059814, 0.7409090399742126, 0.7731424570083618, 0.7703425288200378, 0.8567978739738464, 0.8695580363273621, 0.8232986330986023, 0.7728108167648315, 0.8284820914268494, 0.8809059858322144, 0.8430863618850708, 0.9277614951133728, 0.7059626579284668, 0.2357528805732727, 0.2048046737909317, 0.25032639503479004, 0.37300461530685425, 0.44095587730407715, 0.765160322189331, 0.25759562849998474, 0.14155970513820648, 0.2777196168899536, 0.4416657090187073, 0.6334515810012817, 0.6942825317382812, 0.46738117933273315, 0.336669921875, 0.3979049623012543, 0.5696573257446289, 0.6043677926063538, 0.8601670265197754, 0.8212330937385559, 0.7235438823699951, 0.47798827290534973, 0.6802322864532471, 0.694293737411499, 0.8856753706932068, 0.8542357683181763, 0.7727826237678528, 0.7064467668533325, 0.6220883727073669, 0.6127245426177979, 0.853516161441803, 0.5834574103355408, 0.5752882957458496, 0.583983838558197, 0.6568416953086853, 0.5645153522491455, 0.8171284794807434, 0.49544182419776917, 0.2718471884727478, 0.37361839413642883, 0.517508327960968, 0.5425353646278381, 0.9098472595214844, 0.7383230924606323, 0.5827692747116089, 0.6200133562088013, 0.6831235885620117, 0.8541412949562073, 0.8489861488342285, 0.8372923731803894, 0.7859600186347961, 0.8921899199485779, 0.9318752884864807, 0.9794806241989136, 0.48344188928604126, 0.5110878944396973, 0.8009107112884521, 0.9412023425102234, 0.9828890562057495, 0.9933406710624695, 0.7781630158424377, 0.40406331419944763, 0.2602696418762207, 0.3639248013496399, 0.3311113715171814, 0.33839407563209534, 0.7547736763954163, 0.31178659200668335, 0.23010534048080444, 0.3094087243080139, 0.3914620280265808, 0.5813432931900024, 0.8422772288322449, 0.6358492374420166, 0.45270201563835144, 0.5476433038711548, 0.64478999376297, 0.7974265217781067, 0.8791443109512329, 0.7962453365325928, 0.7315934896469116, 0.8196043372154236, 0.8709262013435364, 0.8843627572059631, 0.842067539691925, 0.8329904079437256, 0.8775254487991333, 0.8707519769668579, 0.9291237592697144, 0.9079189300537109, 0.6876417398452759, 0.2802119255065918, 0.16451920568943024, 0.180746927857399, 0.31985151767730713, 0.43513819575309753, 0.6673773527145386, 0.2852121889591217, 0.13051190972328186, 0.15565386414527893, 0.447013795375824, 0.533351480960846, 0.8369092345237732, 0.4252656400203705, 0.2617509663105011, 0.4149603545665741, 0.570585310459137, 0.6163910031318665, 0.8726122379302979, 0.8443569540977478, 0.6435930132865906, 0.6117346882820129, 0.6625273823738098, 0.6243544816970825, 0.8443796038627625, 0.8325846195220947, 0.7835538983345032, 0.7633504271507263, 0.6655003428459167, 0.6521747708320618] + }, "objects": { "dtype": "uint64", "shape": [3, 5, 6], - "data": [0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1] + "data": [1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "count": 1 + "count": 2 }, { "id": "mutex_logits_classified_min2", - "method": "mutex_watershed", - "block": "aff_small", "output_class": null, - "channels": [0, 1, 2], "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "threshold": 0.4, "min_size": 2, - "connectivity": 1, - "per_slice": false, + "affinities": { + "dtype": "float32", + "shape": [3, 3, 5, 6], + "data": [0.8173035979270935, 0.6709539890289307, 0.5314658880233765, 0.6084961295127869, 0.580284595489502, 0.39640340209007263, 0.8138487339019775, 0.572124183177948, 0.30029675364494324, 0.39452648162841797, 0.6074596643447876, 0.6964578628540039, 0.9113277792930603, 0.5956977009773254, 0.48067590594291687, 0.48869192600250244, 0.6830602288246155, 0.8923346400260925, 0.8191739320755005, 0.7911310791969299, 0.8089491128921509, 0.8573397397994995, 0.931551456451416, 0.9605108499526978, 0.540343165397644, 0.6995229125022888, 0.8376197814941406, 0.9495235681533813, 0.9834397435188293, 0.9828463196754456, 0.8062157034873962, 0.4805664122104645, 0.24930432438850403, 0.14998309314250946, 0.30243417620658875, 0.27364030480384827, 0.7696471214294434, 0.38701876997947693, 0.3045375347137451, 0.3102498948574066, 0.41769421100616455, 0.4155409336090088, 0.8262365460395813, 0.6279906034469604, 0.5950109362602234, 0.524612545967102, 0.7424357533454895, 0.6911601424217224, 0.9068256616592407, 0.7956187725067139, 0.7641294598579407, 0.7723896503448486, 0.8902938961982727, 0.8995878100395203, 0.8448649048805237, 0.7977418303489685, 0.8189159631729126, 0.8688940405845642, 0.9213672280311584, 0.9301550388336182, 0.6071709394454956, 0.29985326528549194, 0.11862216144800186, 0.22468425333499908, 0.35070356726646423, 0.3602716326713562, 0.6548090577125549, 0.357278436422348, 0.13100528717041016, 0.19024153053760529, 0.4414389133453369, 0.5682706832885742, 0.7504817843437195, 0.49855589866638184, 0.2053276002407074, 0.49666181206703186, 0.4698314666748047, 0.6031646132469177, 0.8773792386054993, 0.8162015080451965, 0.6982517242431641, 0.5445089936256409, 0.6130658984184265, 0.6829502582550049, 0.8334912657737732, 0.7786041498184204, 0.8568989634513855, 0.8189595937728882, 0.6456342339515686, 0.6066650152206421, 0.8705799579620361, 0.5936280488967896, 0.5990898013114929, 0.5707679390907288, 0.699874997138977, 0.5258734226226807, 0.8237023949623108, 0.4830091893672943, 0.3440220057964325, 0.39106249809265137, 0.5272782444953918, 0.5401390790939331, 0.90681391954422, 0.6455284953117371, 0.5193508267402649, 0.5085607767105103, 0.6066353917121887, 0.8191872239112854, 0.8544543981552124, 0.8705684542655945, 0.8089056611061096, 0.8915902376174927, 0.9192348122596741, 0.9730367064476013, 0.5532479882240295, 0.6282135248184204, 0.8323439359664917, 0.9617546796798706, 0.9843360185623169, 0.9895327091217041, 0.7972518801689148, 0.43719586730003357, 0.19822420179843903, 0.23676268756389618, 0.24064379930496216, 0.25658270716667175, 0.7170876264572144, 0.3191055655479431, 0.24815791845321655, 0.2125205099582672, 0.6145243048667908, 0.5946095585823059, 0.8729150295257568, 0.6129506230354309, 0.42015886306762695, 0.564541757106781, 0.6118093132972717, 0.7492960691452026, 0.8672401905059814, 0.7409090399742126, 0.7731424570083618, 0.7703425288200378, 0.8567978739738464, 0.8695580363273621, 0.8232986330986023, 0.7728108167648315, 0.8284820914268494, 0.8809059858322144, 0.8430863618850708, 0.9277614951133728, 0.7059626579284668, 0.2357528805732727, 0.2048046737909317, 0.25032639503479004, 0.37300461530685425, 0.44095587730407715, 0.765160322189331, 0.25759562849998474, 0.14155970513820648, 0.2777196168899536, 0.4416657090187073, 0.6334515810012817, 0.6942825317382812, 0.46738117933273315, 0.336669921875, 0.3979049623012543, 0.5696573257446289, 0.6043677926063538, 0.8601670265197754, 0.8212330937385559, 0.7235438823699951, 0.47798827290534973, 0.6802322864532471, 0.694293737411499, 0.8856753706932068, 0.8542357683181763, 0.7727826237678528, 0.7064467668533325, 0.6220883727073669, 0.6127245426177979, 0.853516161441803, 0.5834574103355408, 0.5752882957458496, 0.583983838558197, 0.6568416953086853, 0.5645153522491455, 0.8171284794807434, 0.49544182419776917, 0.2718471884727478, 0.37361839413642883, 0.517508327960968, 0.5425353646278381, 0.9098472595214844, 0.7383230924606323, 0.5827692747116089, 0.6200133562088013, 0.6831235885620117, 0.8541412949562073, 0.8489861488342285, 0.8372923731803894, 0.7859600186347961, 0.8921899199485779, 0.9318752884864807, 0.9794806241989136, 0.48344188928604126, 0.5110878944396973, 0.8009107112884521, 0.9412023425102234, 0.9828890562057495, 0.9933406710624695, 0.7781630158424377, 0.40406331419944763, 0.2602696418762207, 0.3639248013496399, 0.3311113715171814, 0.33839407563209534, 0.7547736763954163, 0.31178659200668335, 0.23010534048080444, 0.3094087243080139, 0.3914620280265808, 0.5813432931900024, 0.8422772288322449, 0.6358492374420166, 0.45270201563835144, 0.5476433038711548, 0.64478999376297, 0.7974265217781067, 0.8791443109512329, 0.7962453365325928, 0.7315934896469116, 0.8196043372154236, 0.8709262013435364, 0.8843627572059631, 0.842067539691925, 0.8329904079437256, 0.8775254487991333, 0.8707519769668579, 0.9291237592697144, 0.9079189300537109, 0.6876417398452759, 0.2802119255065918, 0.16451920568943024, 0.180746927857399, 0.31985151767730713, 0.43513819575309753, 0.6673773527145386, 0.2852121889591217, 0.13051190972328186, 0.15565386414527893, 0.447013795375824, 0.533351480960846, 0.8369092345237732, 0.4252656400203705, 0.2617509663105011, 0.4149603545665741, 0.570585310459137, 0.6163910031318665, 0.8726122379302979, 0.8443569540977478, 0.6435930132865906, 0.6117346882820129, 0.6625273823738098, 0.6243544816970825, 0.8443796038627625, 0.8325846195220947, 0.7835538983345032, 0.7633504271507263, 0.6655003428459167, 0.6521747708320618] + }, "objects": { "dtype": "uint64", "shape": [3, 5, 6], - "data": [1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 0, 0, 0, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 0, 2, 2, 1, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2] - }, - "count": 2 - } - ], - "relabel_objects": [ - { - "id": "wall_cuts", - "existing": { - "dtype": "uint8", - "shape": [3, 5, 8], - "data": [0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [3, 5, 8], - "data": [0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0] - }, - "counts": { - "objects": 2, - "split": 1, - "merged": 0 - } - }, - { - "id": "wall_one_slice", - "existing": { - "dtype": "uint8", - "shape": [3, 5, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [3, 5, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "counts": { - "objects": 1, - "split": 0, - "merged": 0 - } - }, - { - "id": "wall_one_slice_per_slice", - "existing": { - "dtype": "uint8", - "shape": [3, 5, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": true, - "labels": { - "dtype": "uint8", - "shape": [3, 5, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 2, 1, 3, 3, 0, 0, 2, 2, 2, 1, 3, 3, 0, 0, 2, 2, 2, 1, 3, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 4, 4, 0, 0, 4, 4, 4, 4, 4, 4, 0, 0, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "counts": { - "objects": 4, - "split": 3, - "merged": 0 - } - }, - { - "id": "stroke_merges", - "existing": { - "dtype": "uint8", - "shape": [2, 4, 9], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 7, 7, 7, 7, 3, 3, 3, 3, 3, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 7, 7, 7, 7, 3, 3, 3, 3, 3, 7, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [2, 4, 9], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "counts": { - "objects": 1, - "split": 0, - "merged": 1 - } - }, - { - "id": "tie_takes_larger_id", - "existing": { - "dtype": "uint8", - "shape": [1, 2, 4], - "data": [4, 4, 4, 4, 6, 6, 6, 6] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [1, 2, 4], - "data": [6, 6, 6, 6, 6, 6, 6, 6] - }, - "counts": { - "objects": 1, - "split": 0, - "merged": 1 - } - }, - { - "id": "keep_largest_first", - "existing": { - "dtype": "uint8", - "shape": [1, 7, 9], - "data": [5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 4] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [1, 7, 9], - "data": [3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 4] - }, - "counts": { - "objects": 5, - "split": 2, - "merged": 0 - } - }, - { - "id": "diagonal_faces", - "existing": { - "dtype": "uint8", - "shape": [2, 4, 4], - "data": [6, 6, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 6] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [2, 4, 4], - "data": [2, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 6] - }, - "counts": { - "objects": 2, - "split": 1, - "merged": 1 - } - }, - { - "id": "diagonal_edges", - "existing": { - "dtype": "uint8", - "shape": [2, 4, 4], - "data": [6, 6, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 6] - }, - "connectivity": 2, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [2, 4, 4], - "data": [2, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 6] - }, - "counts": { - "objects": 2, - "split": 1, - "merged": 1 - } - }, - { - "id": "diagonal_all", - "existing": { - "dtype": "uint8", - "shape": [2, 4, 4], - "data": [6, 6, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 6] - }, - "connectivity": 3, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [2, 4, 4], - "data": [6, 6, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 0, 6, 6] - }, - "counts": { - "objects": 1, - "split": 0, - "merged": 1 - } - }, - { - "id": "uint16", - "existing": { - "dtype": "uint16", - "shape": [2, 3, 10], - "data": [300, 300, 300, 300, 1, 1, 300, 300, 300, 300, 1, 1, 301, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 300, 300, 300, 300, 1, 1, 300, 300, 300, 300, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint16", - "shape": [2, 3, 10], - "data": [300, 300, 300, 300, 1, 1, 2, 2, 2, 2, 1, 1, 300, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 300, 300, 300, 300, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "counts": { - "objects": 2, - "split": 1, - "merged": 1 - } - }, - { - "id": "no_foreground", - "existing": { - "dtype": "uint8", - "shape": [2, 2, 2], - "data": [1, 1, 1, 1, 0, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": false, - "labels": { - "dtype": "uint8", - "shape": [2, 2, 2], - "data": [1, 1, 1, 1, 0, 0, 0, 0] - }, - "counts": { - "objects": 0, - "split": 0, - "merged": 0 - } - }, - { - "id": "ids_run_out", - "existing": { - "dtype": "uint8", - "shape": [1, 24, 48], - "data": [2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 0, 11, 0, 12, 0, 13, 0, 14, 0, 15, 0, 16, 0, 17, 0, 18, 0, 19, 0, 20, 0, 21, 0, 22, 0, 23, 0, 24, 0, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 27, 0, 28, 0, 29, 0, 30, 0, 31, 0, 32, 0, 33, 0, 34, 0, 35, 0, 36, 0, 37, 0, 38, 0, 39, 0, 40, 0, 41, 0, 42, 0, 43, 0, 44, 0, 45, 0, 46, 0, 47, 0, 48, 0, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 0, 58, 0, 59, 0, 60, 0, 61, 0, 62, 0, 63, 0, 64, 0, 65, 0, 66, 0, 67, 0, 68, 0, 69, 0, 70, 0, 71, 0, 72, 0, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 0, 75, 0, 76, 0, 77, 0, 78, 0, 79, 0, 80, 0, 81, 0, 82, 0, 83, 0, 84, 0, 85, 0, 86, 0, 87, 0, 88, 0, 89, 0, 90, 0, 91, 0, 92, 0, 93, 0, 94, 0, 95, 0, 96, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0, 103, 0, 104, 0, 105, 0, 106, 0, 107, 0, 108, 0, 109, 0, 110, 0, 111, 0, 112, 0, 113, 0, 114, 0, 115, 0, 116, 0, 117, 0, 118, 0, 119, 0, 120, 0, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 122, 0, 123, 0, 124, 0, 125, 0, 126, 0, 127, 0, 128, 0, 129, 0, 130, 0, 131, 0, 132, 0, 133, 0, 134, 0, 135, 0, 136, 0, 137, 0, 138, 0, 139, 0, 140, 0, 141, 0, 142, 0, 143, 0, 144, 0, 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 146, 0, 147, 0, 148, 0, 149, 0, 150, 0, 151, 0, 152, 0, 153, 0, 154, 0, 155, 0, 156, 0, 157, 0, 158, 0, 159, 0, 160, 0, 161, 0, 162, 0, 163, 0, 164, 0, 165, 0, 166, 0, 167, 0, 168, 0, 169, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 0, 171, 0, 172, 0, 173, 0, 174, 0, 175, 0, 176, 0, 177, 0, 178, 0, 179, 0, 180, 0, 181, 0, 182, 0, 183, 0, 184, 0, 185, 0, 186, 0, 187, 0, 188, 0, 189, 0, 190, 0, 191, 0, 192, 0, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 194, 0, 195, 0, 196, 0, 197, 0, 198, 0, 199, 0, 200, 0, 201, 0, 202, 0, 203, 0, 204, 0, 205, 0, 206, 0, 207, 0, 208, 0, 209, 0, 210, 0, 211, 0, 212, 0, 213, 0, 214, 0, 215, 0, 216, 0, 217, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 0, 219, 0, 220, 0, 221, 0, 222, 0, 223, 0, 224, 0, 225, 0, 226, 0, 227, 0, 228, 0, 229, 0, 230, 0, 231, 0, 232, 0, 233, 0, 234, 0, 235, 0, 236, 0, 237, 0, 238, 0, 239, 0, 240, 0, 241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 0, 243, 0, 244, 0, 245, 0, 246, 0, 247, 0, 248, 0, 249, 0, 250, 0, 251, 0, 252, 0, 253, 0, 254, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "connectivity": 1, - "per_slice": false, - "error": "1 new objects do not fit in the volume's uint8 labels: make a new annotation volume, which for an instance model (Cellpose, affinities) now holds uint16 ids, or raise Min size or the threshold so the seed makes fewer objects" - } - ], - "rewrite_foreground": [ - { - "id": "wall_across_chunks", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [1, 1, 1], - "hi": [7, 7, 7], - "connectivity": 1, - "per_slice": false, - "changed": 72, - "counts": { - "objects": 2, - "split": 1, - "merged": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint8", - "shape": [6, 6, 6], - "data": [5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 1, 5, 5] - }, - "after": { - "dtype": "uint8", - "shape": [6, 6, 6], - "data": [5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2, 5, 5, 5, 1, 2, 2] - } - }, - "restore": { - "stroke": [ - [[3, 3, 2], 1], - [[3, 3, 6], 7] - ], - "restored": 71, - "final": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 1, 5, 1, 5, 7, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - }, - { - "id": "joined_nothing_changes", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [1, 1, 1], - "hi": [7, 7, 7], - "connectivity": 1, - "per_slice": false, - "changed": 0, - "counts": { - "objects": 1, - "split": 0, - "merged": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 5, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": null - }, - { - "id": "per_slice", - "dtype": "uint8", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [2, 0, 0], - "hi": [5, 8, 8], - "connectivity": 1, - "per_slice": true, - "changed": 72, - "counts": { - "objects": 6, - "split": 5, - "merged": 0 - }, - "after": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, - "undo": { - "before": { - "dtype": "uint8", - "shape": [3, 8, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "after": { - "dtype": "uint8", - "shape": [3, 8, 8], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 5, 5, 5, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 3, 3, 3, 1, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 6, 6, 6, 1, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - }, - "restore": { - "stroke": [ - [[4, 2, 2], 3] - ], - "restored": 71, - "final": { - "dtype": "uint8", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 3, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 5, 5, 5, 1, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - }, - { - "id": "uint16", - "dtype": "uint16", - "shape": [8, 8, 8], - "chunks": [4, 4, 4], - "initial": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "lo": [0, 0, 0], - "hi": [8, 8, 8], - "connectivity": 1, - "per_slice": false, - "changed": 72, - "counts": { - "objects": 3, - "split": 1, - "merged": 0 - }, - "after": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "undo": { - "before": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - }, - "after": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 400, 400, 400, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - }, - "restore": { - "stroke": [ - [[2, 2, 5], 1] - ], - "restored": 71, - "final": { - "dtype": "uint16", - "shape": [8, 8, 8], - "data": [9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 1, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 400, 400, 400, 1, 400, 400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - } - } - ], - "read_prediction": [ - { - "id": "same_grid", - "served": { - "shape": [2, 12, 12, 12], - "voxel": [8, 8, 8], - "translation": [4, 4, 4], - "chunks": [4, 4, 4] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [2, 3, 1], - "hi": [9, 10, 11], - "prediction": { - "dtype": "float32", - "shape": [2, 7, 7, 10], - "data": [20301.0, 20302.0, 20303.0, 20304.0, 20305.0, 20306.0, 20307.0, 20308.0, 20309.0, 20310.0, 20401.0, 20402.0, 20403.0, 20404.0, 20405.0, 20406.0, 20407.0, 20408.0, 20409.0, 20410.0, 20501.0, 20502.0, 20503.0, 20504.0, 20505.0, 20506.0, 20507.0, 20508.0, 20509.0, 20510.0, 20601.0, 20602.0, 20603.0, 20604.0, 20605.0, 20606.0, 20607.0, 20608.0, 20609.0, 20610.0, 20701.0, 20702.0, 20703.0, 20704.0, 20705.0, 20706.0, 20707.0, 20708.0, 20709.0, 20710.0, 20801.0, 20802.0, 20803.0, 20804.0, 20805.0, 20806.0, 20807.0, 20808.0, 20809.0, 20810.0, 20901.0, 20902.0, 20903.0, 20904.0, 20905.0, 20906.0, 20907.0, 20908.0, 20909.0, 20910.0, 30301.0, 30302.0, 30303.0, 30304.0, 30305.0, 30306.0, 30307.0, 30308.0, 30309.0, 30310.0, 30401.0, 30402.0, 30403.0, 30404.0, 30405.0, 30406.0, 30407.0, 30408.0, 30409.0, 30410.0, 30501.0, 30502.0, 30503.0, 30504.0, 30505.0, 30506.0, 30507.0, 30508.0, 30509.0, 30510.0, 30601.0, 30602.0, 30603.0, 30604.0, 30605.0, 30606.0, 30607.0, 30608.0, 30609.0, 30610.0, 30701.0, 30702.0, 30703.0, 30704.0, 30705.0, 30706.0, 30707.0, 30708.0, 30709.0, 30710.0, 30801.0, 30802.0, 30803.0, 30804.0, 30805.0, 30806.0, 30807.0, 30808.0, 30809.0, 30810.0, 30901.0, 30902.0, 30903.0, 30904.0, 30905.0, 30906.0, 30907.0, 30908.0, 30909.0, 30910.0, 40301.0, 40302.0, 40303.0, 40304.0, 40305.0, 40306.0, 40307.0, 40308.0, 40309.0, 40310.0, 40401.0, 40402.0, 40403.0, 40404.0, 40405.0, 40406.0, 40407.0, 40408.0, 40409.0, 40410.0, 40501.0, 40502.0, 40503.0, 40504.0, 40505.0, 40506.0, 40507.0, 40508.0, 40509.0, 40510.0, 40601.0, 40602.0, 40603.0, 40604.0, 40605.0, 40606.0, 40607.0, 40608.0, 40609.0, 40610.0, 40701.0, 40702.0, 40703.0, 40704.0, 40705.0, 40706.0, 40707.0, 40708.0, 40709.0, 40710.0, 40801.0, 40802.0, 40803.0, 40804.0, 40805.0, 40806.0, 40807.0, 40808.0, 40809.0, 40810.0, 40901.0, 40902.0, 40903.0, 40904.0, 40905.0, 40906.0, 40907.0, 40908.0, 40909.0, 40910.0, 50301.0, 50302.0, 50303.0, 50304.0, 50305.0, 50306.0, 50307.0, 50308.0, 50309.0, 50310.0, 50401.0, 50402.0, 50403.0, 50404.0, 50405.0, 50406.0, 50407.0, 50408.0, 50409.0, 50410.0, 50501.0, 50502.0, 50503.0, 50504.0, 50505.0, 50506.0, 50507.0, 50508.0, 50509.0, 50510.0, 50601.0, 50602.0, 50603.0, 50604.0, 50605.0, 50606.0, 50607.0, 50608.0, 50609.0, 50610.0, 50701.0, 50702.0, 50703.0, 50704.0, 50705.0, 50706.0, 50707.0, 50708.0, 50709.0, 50710.0, 50801.0, 50802.0, 50803.0, 50804.0, 50805.0, 50806.0, 50807.0, 50808.0, 50809.0, 50810.0, 50901.0, 50902.0, 50903.0, 50904.0, 50905.0, 50906.0, 50907.0, 50908.0, 50909.0, 50910.0, 60301.0, 60302.0, 60303.0, 60304.0, 60305.0, 60306.0, 60307.0, 60308.0, 60309.0, 60310.0, 60401.0, 60402.0, 60403.0, 60404.0, 60405.0, 60406.0, 60407.0, 60408.0, 60409.0, 60410.0, 60501.0, 60502.0, 60503.0, 60504.0, 60505.0, 60506.0, 60507.0, 60508.0, 60509.0, 60510.0, 60601.0, 60602.0, 60603.0, 60604.0, 60605.0, 60606.0, 60607.0, 60608.0, 60609.0, 60610.0, 60701.0, 60702.0, 60703.0, 60704.0, 60705.0, 60706.0, 60707.0, 60708.0, 60709.0, 60710.0, 60801.0, 60802.0, 60803.0, 60804.0, 60805.0, 60806.0, 60807.0, 60808.0, 60809.0, 60810.0, 60901.0, 60902.0, 60903.0, 60904.0, 60905.0, 60906.0, 60907.0, 60908.0, 60909.0, 60910.0, 70301.0, 70302.0, 70303.0, 70304.0, 70305.0, 70306.0, 70307.0, 70308.0, 70309.0, 70310.0, 70401.0, 70402.0, 70403.0, 70404.0, 70405.0, 70406.0, 70407.0, 70408.0, 70409.0, 70410.0, 70501.0, 70502.0, 70503.0, 70504.0, 70505.0, 70506.0, 70507.0, 70508.0, 70509.0, 70510.0, 70601.0, 70602.0, 70603.0, 70604.0, 70605.0, 70606.0, 70607.0, 70608.0, 70609.0, 70610.0, 70701.0, 70702.0, 70703.0, 70704.0, 70705.0, 70706.0, 70707.0, 70708.0, 70709.0, 70710.0, 70801.0, 70802.0, 70803.0, 70804.0, 70805.0, 70806.0, 70807.0, 70808.0, 70809.0, 70810.0, 70901.0, 70902.0, 70903.0, 70904.0, 70905.0, 70906.0, 70907.0, 70908.0, 70909.0, 70910.0, 80301.0, 80302.0, 80303.0, 80304.0, 80305.0, 80306.0, 80307.0, 80308.0, 80309.0, 80310.0, 80401.0, 80402.0, 80403.0, 80404.0, 80405.0, 80406.0, 80407.0, 80408.0, 80409.0, 80410.0, 80501.0, 80502.0, 80503.0, 80504.0, 80505.0, 80506.0, 80507.0, 80508.0, 80509.0, 80510.0, 80601.0, 80602.0, 80603.0, 80604.0, 80605.0, 80606.0, 80607.0, 80608.0, 80609.0, 80610.0, 80701.0, 80702.0, 80703.0, 80704.0, 80705.0, 80706.0, 80707.0, 80708.0, 80709.0, 80710.0, 80801.0, 80802.0, 80803.0, 80804.0, 80805.0, 80806.0, 80807.0, 80808.0, 80809.0, 80810.0, 80901.0, 80902.0, 80903.0, 80904.0, 80905.0, 80906.0, 80907.0, 80908.0, 80909.0, 80910.0, 1020301.0, 1020302.0, 1020303.0, 1020304.0, 1020305.0, 1020306.0, 1020307.0, 1020308.0, 1020309.0, 1020310.0, 1020401.0, 1020402.0, 1020403.0, 1020404.0, 1020405.0, 1020406.0, 1020407.0, 1020408.0, 1020409.0, 1020410.0, 1020501.0, 1020502.0, 1020503.0, 1020504.0, 1020505.0, 1020506.0, 1020507.0, 1020508.0, 1020509.0, 1020510.0, 1020601.0, 1020602.0, 1020603.0, 1020604.0, 1020605.0, 1020606.0, 1020607.0, 1020608.0, 1020609.0, 1020610.0, 1020701.0, 1020702.0, 1020703.0, 1020704.0, 1020705.0, 1020706.0, 1020707.0, 1020708.0, 1020709.0, 1020710.0, 1020801.0, 1020802.0, 1020803.0, 1020804.0, 1020805.0, 1020806.0, 1020807.0, 1020808.0, 1020809.0, 1020810.0, 1020901.0, 1020902.0, 1020903.0, 1020904.0, 1020905.0, 1020906.0, 1020907.0, 1020908.0, 1020909.0, 1020910.0, 1030301.0, 1030302.0, 1030303.0, 1030304.0, 1030305.0, 1030306.0, 1030307.0, 1030308.0, 1030309.0, 1030310.0, 1030401.0, 1030402.0, 1030403.0, 1030404.0, 1030405.0, 1030406.0, 1030407.0, 1030408.0, 1030409.0, 1030410.0, 1030501.0, 1030502.0, 1030503.0, 1030504.0, 1030505.0, 1030506.0, 1030507.0, 1030508.0, 1030509.0, 1030510.0, 1030601.0, 1030602.0, 1030603.0, 1030604.0, 1030605.0, 1030606.0, 1030607.0, 1030608.0, 1030609.0, 1030610.0, 1030701.0, 1030702.0, 1030703.0, 1030704.0, 1030705.0, 1030706.0, 1030707.0, 1030708.0, 1030709.0, 1030710.0, 1030801.0, 1030802.0, 1030803.0, 1030804.0, 1030805.0, 1030806.0, 1030807.0, 1030808.0, 1030809.0, 1030810.0, 1030901.0, 1030902.0, 1030903.0, 1030904.0, 1030905.0, 1030906.0, 1030907.0, 1030908.0, 1030909.0, 1030910.0, 1040301.0, 1040302.0, 1040303.0, 1040304.0, 1040305.0, 1040306.0, 1040307.0, 1040308.0, 1040309.0, 1040310.0, 1040401.0, 1040402.0, 1040403.0, 1040404.0, 1040405.0, 1040406.0, 1040407.0, 1040408.0, 1040409.0, 1040410.0, 1040501.0, 1040502.0, 1040503.0, 1040504.0, 1040505.0, 1040506.0, 1040507.0, 1040508.0, 1040509.0, 1040510.0, 1040601.0, 1040602.0, 1040603.0, 1040604.0, 1040605.0, 1040606.0, 1040607.0, 1040608.0, 1040609.0, 1040610.0, 1040701.0, 1040702.0, 1040703.0, 1040704.0, 1040705.0, 1040706.0, 1040707.0, 1040708.0, 1040709.0, 1040710.0, 1040801.0, 1040802.0, 1040803.0, 1040804.0, 1040805.0, 1040806.0, 1040807.0, 1040808.0, 1040809.0, 1040810.0, 1040901.0, 1040902.0, 1040903.0, 1040904.0, 1040905.0, 1040906.0, 1040907.0, 1040908.0, 1040909.0, 1040910.0, 1050301.0, 1050302.0, 1050303.0, 1050304.0, 1050305.0, 1050306.0, 1050307.0, 1050308.0, 1050309.0, 1050310.0, 1050401.0, 1050402.0, 1050403.0, 1050404.0, 1050405.0, 1050406.0, 1050407.0, 1050408.0, 1050409.0, 1050410.0, 1050501.0, 1050502.0, 1050503.0, 1050504.0, 1050505.0, 1050506.0, 1050507.0, 1050508.0, 1050509.0, 1050510.0, 1050601.0, 1050602.0, 1050603.0, 1050604.0, 1050605.0, 1050606.0, 1050607.0, 1050608.0, 1050609.0, 1050610.0, 1050701.0, 1050702.0, 1050703.0, 1050704.0, 1050705.0, 1050706.0, 1050707.0, 1050708.0, 1050709.0, 1050710.0, 1050801.0, 1050802.0, 1050803.0, 1050804.0, 1050805.0, 1050806.0, 1050807.0, 1050808.0, 1050809.0, 1050810.0, 1050901.0, 1050902.0, 1050903.0, 1050904.0, 1050905.0, 1050906.0, 1050907.0, 1050908.0, 1050909.0, 1050910.0, 1060301.0, 1060302.0, 1060303.0, 1060304.0, 1060305.0, 1060306.0, 1060307.0, 1060308.0, 1060309.0, 1060310.0, 1060401.0, 1060402.0, 1060403.0, 1060404.0, 1060405.0, 1060406.0, 1060407.0, 1060408.0, 1060409.0, 1060410.0, 1060501.0, 1060502.0, 1060503.0, 1060504.0, 1060505.0, 1060506.0, 1060507.0, 1060508.0, 1060509.0, 1060510.0, 1060601.0, 1060602.0, 1060603.0, 1060604.0, 1060605.0, 1060606.0, 1060607.0, 1060608.0, 1060609.0, 1060610.0, 1060701.0, 1060702.0, 1060703.0, 1060704.0, 1060705.0, 1060706.0, 1060707.0, 1060708.0, 1060709.0, 1060710.0, 1060801.0, 1060802.0, 1060803.0, 1060804.0, 1060805.0, 1060806.0, 1060807.0, 1060808.0, 1060809.0, 1060810.0, 1060901.0, 1060902.0, 1060903.0, 1060904.0, 1060905.0, 1060906.0, 1060907.0, 1060908.0, 1060909.0, 1060910.0, 1070301.0, 1070302.0, 1070303.0, 1070304.0, 1070305.0, 1070306.0, 1070307.0, 1070308.0, 1070309.0, 1070310.0, 1070401.0, 1070402.0, 1070403.0, 1070404.0, 1070405.0, 1070406.0, 1070407.0, 1070408.0, 1070409.0, 1070410.0, 1070501.0, 1070502.0, 1070503.0, 1070504.0, 1070505.0, 1070506.0, 1070507.0, 1070508.0, 1070509.0, 1070510.0, 1070601.0, 1070602.0, 1070603.0, 1070604.0, 1070605.0, 1070606.0, 1070607.0, 1070608.0, 1070609.0, 1070610.0, 1070701.0, 1070702.0, 1070703.0, 1070704.0, 1070705.0, 1070706.0, 1070707.0, 1070708.0, 1070709.0, 1070710.0, 1070801.0, 1070802.0, 1070803.0, 1070804.0, 1070805.0, 1070806.0, 1070807.0, 1070808.0, 1070809.0, 1070810.0, 1070901.0, 1070902.0, 1070903.0, 1070904.0, 1070905.0, 1070906.0, 1070907.0, 1070908.0, 1070909.0, 1070910.0, 1080301.0, 1080302.0, 1080303.0, 1080304.0, 1080305.0, 1080306.0, 1080307.0, 1080308.0, 1080309.0, 1080310.0, 1080401.0, 1080402.0, 1080403.0, 1080404.0, 1080405.0, 1080406.0, 1080407.0, 1080408.0, 1080409.0, 1080410.0, 1080501.0, 1080502.0, 1080503.0, 1080504.0, 1080505.0, 1080506.0, 1080507.0, 1080508.0, 1080509.0, 1080510.0, 1080601.0, 1080602.0, 1080603.0, 1080604.0, 1080605.0, 1080606.0, 1080607.0, 1080608.0, 1080609.0, 1080610.0, 1080701.0, 1080702.0, 1080703.0, 1080704.0, 1080705.0, 1080706.0, 1080707.0, 1080708.0, 1080709.0, 1080710.0, 1080801.0, 1080802.0, 1080803.0, 1080804.0, 1080805.0, 1080806.0, 1080807.0, 1080808.0, 1080809.0, 1080810.0, 1080901.0, 1080902.0, 1080903.0, 1080904.0, 1080905.0, 1080906.0, 1080907.0, 1080908.0, 1080909.0, 1080910.0] - }, - "new_lo": [2, 3, 1], - "new_hi": [9, 10, 11] - }, - { - "id": "coarser_served", - "served": { - "shape": [1, 8, 8, 8], - "voxel": [16, 16, 16], - "translation": [8, 8, 8], - "chunks": [4, 4, 4] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [1, 2, 3], - "hi": [9, 12, 15], - "prediction": { - "dtype": "float32", - "shape": [1, 8, 10, 12], - "data": [101.0, 102.0, 102.0, 103.0, 103.0, 104.0, 104.0, 105.0, 105.0, 106.0, 106.0, 107.0, 101.0, 102.0, 102.0, 103.0, 103.0, 104.0, 104.0, 105.0, 105.0, 106.0, 106.0, 107.0, 201.0, 202.0, 202.0, 203.0, 203.0, 204.0, 204.0, 205.0, 205.0, 206.0, 206.0, 207.0, 201.0, 202.0, 202.0, 203.0, 203.0, 204.0, 204.0, 205.0, 205.0, 206.0, 206.0, 207.0, 301.0, 302.0, 302.0, 303.0, 303.0, 304.0, 304.0, 305.0, 305.0, 306.0, 306.0, 307.0, 301.0, 302.0, 302.0, 303.0, 303.0, 304.0, 304.0, 305.0, 305.0, 306.0, 306.0, 307.0, 401.0, 402.0, 402.0, 403.0, 403.0, 404.0, 404.0, 405.0, 405.0, 406.0, 406.0, 407.0, 401.0, 402.0, 402.0, 403.0, 403.0, 404.0, 404.0, 405.0, 405.0, 406.0, 406.0, 407.0, 501.0, 502.0, 502.0, 503.0, 503.0, 504.0, 504.0, 505.0, 505.0, 506.0, 506.0, 507.0, 501.0, 502.0, 502.0, 503.0, 503.0, 504.0, 504.0, 505.0, 505.0, 506.0, 506.0, 507.0, 10101.0, 10102.0, 10102.0, 10103.0, 10103.0, 10104.0, 10104.0, 10105.0, 10105.0, 10106.0, 10106.0, 10107.0, 10101.0, 10102.0, 10102.0, 10103.0, 10103.0, 10104.0, 10104.0, 10105.0, 10105.0, 10106.0, 10106.0, 10107.0, 10201.0, 10202.0, 10202.0, 10203.0, 10203.0, 10204.0, 10204.0, 10205.0, 10205.0, 10206.0, 10206.0, 10207.0, 10201.0, 10202.0, 10202.0, 10203.0, 10203.0, 10204.0, 10204.0, 10205.0, 10205.0, 10206.0, 10206.0, 10207.0, 10301.0, 10302.0, 10302.0, 10303.0, 10303.0, 10304.0, 10304.0, 10305.0, 10305.0, 10306.0, 10306.0, 10307.0, 10301.0, 10302.0, 10302.0, 10303.0, 10303.0, 10304.0, 10304.0, 10305.0, 10305.0, 10306.0, 10306.0, 10307.0, 10401.0, 10402.0, 10402.0, 10403.0, 10403.0, 10404.0, 10404.0, 10405.0, 10405.0, 10406.0, 10406.0, 10407.0, 10401.0, 10402.0, 10402.0, 10403.0, 10403.0, 10404.0, 10404.0, 10405.0, 10405.0, 10406.0, 10406.0, 10407.0, 10501.0, 10502.0, 10502.0, 10503.0, 10503.0, 10504.0, 10504.0, 10505.0, 10505.0, 10506.0, 10506.0, 10507.0, 10501.0, 10502.0, 10502.0, 10503.0, 10503.0, 10504.0, 10504.0, 10505.0, 10505.0, 10506.0, 10506.0, 10507.0, 10101.0, 10102.0, 10102.0, 10103.0, 10103.0, 10104.0, 10104.0, 10105.0, 10105.0, 10106.0, 10106.0, 10107.0, 10101.0, 10102.0, 10102.0, 10103.0, 10103.0, 10104.0, 10104.0, 10105.0, 10105.0, 10106.0, 10106.0, 10107.0, 10201.0, 10202.0, 10202.0, 10203.0, 10203.0, 10204.0, 10204.0, 10205.0, 10205.0, 10206.0, 10206.0, 10207.0, 10201.0, 10202.0, 10202.0, 10203.0, 10203.0, 10204.0, 10204.0, 10205.0, 10205.0, 10206.0, 10206.0, 10207.0, 10301.0, 10302.0, 10302.0, 10303.0, 10303.0, 10304.0, 10304.0, 10305.0, 10305.0, 10306.0, 10306.0, 10307.0, 10301.0, 10302.0, 10302.0, 10303.0, 10303.0, 10304.0, 10304.0, 10305.0, 10305.0, 10306.0, 10306.0, 10307.0, 10401.0, 10402.0, 10402.0, 10403.0, 10403.0, 10404.0, 10404.0, 10405.0, 10405.0, 10406.0, 10406.0, 10407.0, 10401.0, 10402.0, 10402.0, 10403.0, 10403.0, 10404.0, 10404.0, 10405.0, 10405.0, 10406.0, 10406.0, 10407.0, 10501.0, 10502.0, 10502.0, 10503.0, 10503.0, 10504.0, 10504.0, 10505.0, 10505.0, 10506.0, 10506.0, 10507.0, 10501.0, 10502.0, 10502.0, 10503.0, 10503.0, 10504.0, 10504.0, 10505.0, 10505.0, 10506.0, 10506.0, 10507.0, 20101.0, 20102.0, 20102.0, 20103.0, 20103.0, 20104.0, 20104.0, 20105.0, 20105.0, 20106.0, 20106.0, 20107.0, 20101.0, 20102.0, 20102.0, 20103.0, 20103.0, 20104.0, 20104.0, 20105.0, 20105.0, 20106.0, 20106.0, 20107.0, 20201.0, 20202.0, 20202.0, 20203.0, 20203.0, 20204.0, 20204.0, 20205.0, 20205.0, 20206.0, 20206.0, 20207.0, 20201.0, 20202.0, 20202.0, 20203.0, 20203.0, 20204.0, 20204.0, 20205.0, 20205.0, 20206.0, 20206.0, 20207.0, 20301.0, 20302.0, 20302.0, 20303.0, 20303.0, 20304.0, 20304.0, 20305.0, 20305.0, 20306.0, 20306.0, 20307.0, 20301.0, 20302.0, 20302.0, 20303.0, 20303.0, 20304.0, 20304.0, 20305.0, 20305.0, 20306.0, 20306.0, 20307.0, 20401.0, 20402.0, 20402.0, 20403.0, 20403.0, 20404.0, 20404.0, 20405.0, 20405.0, 20406.0, 20406.0, 20407.0, 20401.0, 20402.0, 20402.0, 20403.0, 20403.0, 20404.0, 20404.0, 20405.0, 20405.0, 20406.0, 20406.0, 20407.0, 20501.0, 20502.0, 20502.0, 20503.0, 20503.0, 20504.0, 20504.0, 20505.0, 20505.0, 20506.0, 20506.0, 20507.0, 20501.0, 20502.0, 20502.0, 20503.0, 20503.0, 20504.0, 20504.0, 20505.0, 20505.0, 20506.0, 20506.0, 20507.0, 20101.0, 20102.0, 20102.0, 20103.0, 20103.0, 20104.0, 20104.0, 20105.0, 20105.0, 20106.0, 20106.0, 20107.0, 20101.0, 20102.0, 20102.0, 20103.0, 20103.0, 20104.0, 20104.0, 20105.0, 20105.0, 20106.0, 20106.0, 20107.0, 20201.0, 20202.0, 20202.0, 20203.0, 20203.0, 20204.0, 20204.0, 20205.0, 20205.0, 20206.0, 20206.0, 20207.0, 20201.0, 20202.0, 20202.0, 20203.0, 20203.0, 20204.0, 20204.0, 20205.0, 20205.0, 20206.0, 20206.0, 20207.0, 20301.0, 20302.0, 20302.0, 20303.0, 20303.0, 20304.0, 20304.0, 20305.0, 20305.0, 20306.0, 20306.0, 20307.0, 20301.0, 20302.0, 20302.0, 20303.0, 20303.0, 20304.0, 20304.0, 20305.0, 20305.0, 20306.0, 20306.0, 20307.0, 20401.0, 20402.0, 20402.0, 20403.0, 20403.0, 20404.0, 20404.0, 20405.0, 20405.0, 20406.0, 20406.0, 20407.0, 20401.0, 20402.0, 20402.0, 20403.0, 20403.0, 20404.0, 20404.0, 20405.0, 20405.0, 20406.0, 20406.0, 20407.0, 20501.0, 20502.0, 20502.0, 20503.0, 20503.0, 20504.0, 20504.0, 20505.0, 20505.0, 20506.0, 20506.0, 20507.0, 20501.0, 20502.0, 20502.0, 20503.0, 20503.0, 20504.0, 20504.0, 20505.0, 20505.0, 20506.0, 20506.0, 20507.0, 30101.0, 30102.0, 30102.0, 30103.0, 30103.0, 30104.0, 30104.0, 30105.0, 30105.0, 30106.0, 30106.0, 30107.0, 30101.0, 30102.0, 30102.0, 30103.0, 30103.0, 30104.0, 30104.0, 30105.0, 30105.0, 30106.0, 30106.0, 30107.0, 30201.0, 30202.0, 30202.0, 30203.0, 30203.0, 30204.0, 30204.0, 30205.0, 30205.0, 30206.0, 30206.0, 30207.0, 30201.0, 30202.0, 30202.0, 30203.0, 30203.0, 30204.0, 30204.0, 30205.0, 30205.0, 30206.0, 30206.0, 30207.0, 30301.0, 30302.0, 30302.0, 30303.0, 30303.0, 30304.0, 30304.0, 30305.0, 30305.0, 30306.0, 30306.0, 30307.0, 30301.0, 30302.0, 30302.0, 30303.0, 30303.0, 30304.0, 30304.0, 30305.0, 30305.0, 30306.0, 30306.0, 30307.0, 30401.0, 30402.0, 30402.0, 30403.0, 30403.0, 30404.0, 30404.0, 30405.0, 30405.0, 30406.0, 30406.0, 30407.0, 30401.0, 30402.0, 30402.0, 30403.0, 30403.0, 30404.0, 30404.0, 30405.0, 30405.0, 30406.0, 30406.0, 30407.0, 30501.0, 30502.0, 30502.0, 30503.0, 30503.0, 30504.0, 30504.0, 30505.0, 30505.0, 30506.0, 30506.0, 30507.0, 30501.0, 30502.0, 30502.0, 30503.0, 30503.0, 30504.0, 30504.0, 30505.0, 30505.0, 30506.0, 30506.0, 30507.0, 30101.0, 30102.0, 30102.0, 30103.0, 30103.0, 30104.0, 30104.0, 30105.0, 30105.0, 30106.0, 30106.0, 30107.0, 30101.0, 30102.0, 30102.0, 30103.0, 30103.0, 30104.0, 30104.0, 30105.0, 30105.0, 30106.0, 30106.0, 30107.0, 30201.0, 30202.0, 30202.0, 30203.0, 30203.0, 30204.0, 30204.0, 30205.0, 30205.0, 30206.0, 30206.0, 30207.0, 30201.0, 30202.0, 30202.0, 30203.0, 30203.0, 30204.0, 30204.0, 30205.0, 30205.0, 30206.0, 30206.0, 30207.0, 30301.0, 30302.0, 30302.0, 30303.0, 30303.0, 30304.0, 30304.0, 30305.0, 30305.0, 30306.0, 30306.0, 30307.0, 30301.0, 30302.0, 30302.0, 30303.0, 30303.0, 30304.0, 30304.0, 30305.0, 30305.0, 30306.0, 30306.0, 30307.0, 30401.0, 30402.0, 30402.0, 30403.0, 30403.0, 30404.0, 30404.0, 30405.0, 30405.0, 30406.0, 30406.0, 30407.0, 30401.0, 30402.0, 30402.0, 30403.0, 30403.0, 30404.0, 30404.0, 30405.0, 30405.0, 30406.0, 30406.0, 30407.0, 30501.0, 30502.0, 30502.0, 30503.0, 30503.0, 30504.0, 30504.0, 30505.0, 30505.0, 30506.0, 30506.0, 30507.0, 30501.0, 30502.0, 30502.0, 30503.0, 30503.0, 30504.0, 30504.0, 30505.0, 30505.0, 30506.0, 30506.0, 30507.0, 40101.0, 40102.0, 40102.0, 40103.0, 40103.0, 40104.0, 40104.0, 40105.0, 40105.0, 40106.0, 40106.0, 40107.0, 40101.0, 40102.0, 40102.0, 40103.0, 40103.0, 40104.0, 40104.0, 40105.0, 40105.0, 40106.0, 40106.0, 40107.0, 40201.0, 40202.0, 40202.0, 40203.0, 40203.0, 40204.0, 40204.0, 40205.0, 40205.0, 40206.0, 40206.0, 40207.0, 40201.0, 40202.0, 40202.0, 40203.0, 40203.0, 40204.0, 40204.0, 40205.0, 40205.0, 40206.0, 40206.0, 40207.0, 40301.0, 40302.0, 40302.0, 40303.0, 40303.0, 40304.0, 40304.0, 40305.0, 40305.0, 40306.0, 40306.0, 40307.0, 40301.0, 40302.0, 40302.0, 40303.0, 40303.0, 40304.0, 40304.0, 40305.0, 40305.0, 40306.0, 40306.0, 40307.0, 40401.0, 40402.0, 40402.0, 40403.0, 40403.0, 40404.0, 40404.0, 40405.0, 40405.0, 40406.0, 40406.0, 40407.0, 40401.0, 40402.0, 40402.0, 40403.0, 40403.0, 40404.0, 40404.0, 40405.0, 40405.0, 40406.0, 40406.0, 40407.0, 40501.0, 40502.0, 40502.0, 40503.0, 40503.0, 40504.0, 40504.0, 40505.0, 40505.0, 40506.0, 40506.0, 40507.0, 40501.0, 40502.0, 40502.0, 40503.0, 40503.0, 40504.0, 40504.0, 40505.0, 40505.0, 40506.0, 40506.0, 40507.0] - }, - "new_lo": [1, 2, 3], - "new_hi": [9, 12, 15] - }, - { - "id": "finer_served", - "served": { - "shape": [1, 20, 20, 20], - "voxel": [4, 4, 4], - "translation": [2, 2, 2], - "chunks": [5, 5, 5] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [0, 1, 2], - "hi": [6, 7, 9], - "prediction": { - "dtype": "float32", - "shape": [1, 6, 6, 7], - "data": [10305.0, 10307.0, 10309.0, 10311.0, 10313.0, 10315.0, 10317.0, 10505.0, 10507.0, 10509.0, 10511.0, 10513.0, 10515.0, 10517.0, 10705.0, 10707.0, 10709.0, 10711.0, 10713.0, 10715.0, 10717.0, 10905.0, 10907.0, 10909.0, 10911.0, 10913.0, 10915.0, 10917.0, 11105.0, 11107.0, 11109.0, 11111.0, 11113.0, 11115.0, 11117.0, 11305.0, 11307.0, 11309.0, 11311.0, 11313.0, 11315.0, 11317.0, 30305.0, 30307.0, 30309.0, 30311.0, 30313.0, 30315.0, 30317.0, 30505.0, 30507.0, 30509.0, 30511.0, 30513.0, 30515.0, 30517.0, 30705.0, 30707.0, 30709.0, 30711.0, 30713.0, 30715.0, 30717.0, 30905.0, 30907.0, 30909.0, 30911.0, 30913.0, 30915.0, 30917.0, 31105.0, 31107.0, 31109.0, 31111.0, 31113.0, 31115.0, 31117.0, 31305.0, 31307.0, 31309.0, 31311.0, 31313.0, 31315.0, 31317.0, 50305.0, 50307.0, 50309.0, 50311.0, 50313.0, 50315.0, 50317.0, 50505.0, 50507.0, 50509.0, 50511.0, 50513.0, 50515.0, 50517.0, 50705.0, 50707.0, 50709.0, 50711.0, 50713.0, 50715.0, 50717.0, 50905.0, 50907.0, 50909.0, 50911.0, 50913.0, 50915.0, 50917.0, 51105.0, 51107.0, 51109.0, 51111.0, 51113.0, 51115.0, 51117.0, 51305.0, 51307.0, 51309.0, 51311.0, 51313.0, 51315.0, 51317.0, 70305.0, 70307.0, 70309.0, 70311.0, 70313.0, 70315.0, 70317.0, 70505.0, 70507.0, 70509.0, 70511.0, 70513.0, 70515.0, 70517.0, 70705.0, 70707.0, 70709.0, 70711.0, 70713.0, 70715.0, 70717.0, 70905.0, 70907.0, 70909.0, 70911.0, 70913.0, 70915.0, 70917.0, 71105.0, 71107.0, 71109.0, 71111.0, 71113.0, 71115.0, 71117.0, 71305.0, 71307.0, 71309.0, 71311.0, 71313.0, 71315.0, 71317.0, 90305.0, 90307.0, 90309.0, 90311.0, 90313.0, 90315.0, 90317.0, 90505.0, 90507.0, 90509.0, 90511.0, 90513.0, 90515.0, 90517.0, 90705.0, 90707.0, 90709.0, 90711.0, 90713.0, 90715.0, 90717.0, 90905.0, 90907.0, 90909.0, 90911.0, 90913.0, 90915.0, 90917.0, 91105.0, 91107.0, 91109.0, 91111.0, 91113.0, 91115.0, 91117.0, 91305.0, 91307.0, 91309.0, 91311.0, 91313.0, 91315.0, 91317.0, 110305.0, 110307.0, 110309.0, 110311.0, 110313.0, 110315.0, 110317.0, 110505.0, 110507.0, 110509.0, 110511.0, 110513.0, 110515.0, 110517.0, 110705.0, 110707.0, 110709.0, 110711.0, 110713.0, 110715.0, 110717.0, 110905.0, 110907.0, 110909.0, 110911.0, 110913.0, 110915.0, 110917.0, 111105.0, 111107.0, 111109.0, 111111.0, 111113.0, 111115.0, 111117.0, 111305.0, 111307.0, 111309.0, 111311.0, 111313.0, 111315.0, 111317.0] - }, - "new_lo": [0, 1, 2], - "new_hi": [6, 7, 9] - }, - { - "id": "offset_served_clips_low", - "served": { - "shape": [1, 10, 10, 10], - "voxel": [8, 8, 8], - "translation": [24, 24, 24], - "chunks": [4, 4, 4] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [0, 1, 2], - "hi": [6, 7, 8], - "prediction": { - "dtype": "float32", - "shape": [1, 4, 5, 6], - "data": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 200.0, 201.0, 202.0, 203.0, 204.0, 205.0, 300.0, 301.0, 302.0, 303.0, 304.0, 305.0, 400.0, 401.0, 402.0, 403.0, 404.0, 405.0, 10000.0, 10001.0, 10002.0, 10003.0, 10004.0, 10005.0, 10100.0, 10101.0, 10102.0, 10103.0, 10104.0, 10105.0, 10200.0, 10201.0, 10202.0, 10203.0, 10204.0, 10205.0, 10300.0, 10301.0, 10302.0, 10303.0, 10304.0, 10305.0, 10400.0, 10401.0, 10402.0, 10403.0, 10404.0, 10405.0, 20000.0, 20001.0, 20002.0, 20003.0, 20004.0, 20005.0, 20100.0, 20101.0, 20102.0, 20103.0, 20104.0, 20105.0, 20200.0, 20201.0, 20202.0, 20203.0, 20204.0, 20205.0, 20300.0, 20301.0, 20302.0, 20303.0, 20304.0, 20305.0, 20400.0, 20401.0, 20402.0, 20403.0, 20404.0, 20405.0, 30000.0, 30001.0, 30002.0, 30003.0, 30004.0, 30005.0, 30100.0, 30101.0, 30102.0, 30103.0, 30104.0, 30105.0, 30200.0, 30201.0, 30202.0, 30203.0, 30204.0, 30205.0, 30300.0, 30301.0, 30302.0, 30303.0, 30304.0, 30305.0, 30400.0, 30401.0, 30402.0, 30403.0, 30404.0, 30405.0] - }, - "new_lo": [2, 2, 2], - "new_hi": [6, 7, 8] - }, - { - "id": "half_voxel_shift", - "served": { - "shape": [1, 10, 10, 10], - "voxel": [8, 8, 8], - "translation": [8, 8, 8], - "chunks": [5, 5, 5] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [1, 1, 1], - "hi": [8, 8, 8], - "prediction": { - "dtype": "float32", - "shape": [1, 7, 7, 7], - "data": [10101.0, 10102.0, 10103.0, 10104.0, 10105.0, 10106.0, 10107.0, 10201.0, 10202.0, 10203.0, 10204.0, 10205.0, 10206.0, 10207.0, 10301.0, 10302.0, 10303.0, 10304.0, 10305.0, 10306.0, 10307.0, 10401.0, 10402.0, 10403.0, 10404.0, 10405.0, 10406.0, 10407.0, 10501.0, 10502.0, 10503.0, 10504.0, 10505.0, 10506.0, 10507.0, 10601.0, 10602.0, 10603.0, 10604.0, 10605.0, 10606.0, 10607.0, 10701.0, 10702.0, 10703.0, 10704.0, 10705.0, 10706.0, 10707.0, 20101.0, 20102.0, 20103.0, 20104.0, 20105.0, 20106.0, 20107.0, 20201.0, 20202.0, 20203.0, 20204.0, 20205.0, 20206.0, 20207.0, 20301.0, 20302.0, 20303.0, 20304.0, 20305.0, 20306.0, 20307.0, 20401.0, 20402.0, 20403.0, 20404.0, 20405.0, 20406.0, 20407.0, 20501.0, 20502.0, 20503.0, 20504.0, 20505.0, 20506.0, 20507.0, 20601.0, 20602.0, 20603.0, 20604.0, 20605.0, 20606.0, 20607.0, 20701.0, 20702.0, 20703.0, 20704.0, 20705.0, 20706.0, 20707.0, 30101.0, 30102.0, 30103.0, 30104.0, 30105.0, 30106.0, 30107.0, 30201.0, 30202.0, 30203.0, 30204.0, 30205.0, 30206.0, 30207.0, 30301.0, 30302.0, 30303.0, 30304.0, 30305.0, 30306.0, 30307.0, 30401.0, 30402.0, 30403.0, 30404.0, 30405.0, 30406.0, 30407.0, 30501.0, 30502.0, 30503.0, 30504.0, 30505.0, 30506.0, 30507.0, 30601.0, 30602.0, 30603.0, 30604.0, 30605.0, 30606.0, 30607.0, 30701.0, 30702.0, 30703.0, 30704.0, 30705.0, 30706.0, 30707.0, 40101.0, 40102.0, 40103.0, 40104.0, 40105.0, 40106.0, 40107.0, 40201.0, 40202.0, 40203.0, 40204.0, 40205.0, 40206.0, 40207.0, 40301.0, 40302.0, 40303.0, 40304.0, 40305.0, 40306.0, 40307.0, 40401.0, 40402.0, 40403.0, 40404.0, 40405.0, 40406.0, 40407.0, 40501.0, 40502.0, 40503.0, 40504.0, 40505.0, 40506.0, 40507.0, 40601.0, 40602.0, 40603.0, 40604.0, 40605.0, 40606.0, 40607.0, 40701.0, 40702.0, 40703.0, 40704.0, 40705.0, 40706.0, 40707.0, 50101.0, 50102.0, 50103.0, 50104.0, 50105.0, 50106.0, 50107.0, 50201.0, 50202.0, 50203.0, 50204.0, 50205.0, 50206.0, 50207.0, 50301.0, 50302.0, 50303.0, 50304.0, 50305.0, 50306.0, 50307.0, 50401.0, 50402.0, 50403.0, 50404.0, 50405.0, 50406.0, 50407.0, 50501.0, 50502.0, 50503.0, 50504.0, 50505.0, 50506.0, 50507.0, 50601.0, 50602.0, 50603.0, 50604.0, 50605.0, 50606.0, 50607.0, 50701.0, 50702.0, 50703.0, 50704.0, 50705.0, 50706.0, 50707.0, 60101.0, 60102.0, 60103.0, 60104.0, 60105.0, 60106.0, 60107.0, 60201.0, 60202.0, 60203.0, 60204.0, 60205.0, 60206.0, 60207.0, 60301.0, 60302.0, 60303.0, 60304.0, 60305.0, 60306.0, 60307.0, 60401.0, 60402.0, 60403.0, 60404.0, 60405.0, 60406.0, 60407.0, 60501.0, 60502.0, 60503.0, 60504.0, 60505.0, 60506.0, 60507.0, 60601.0, 60602.0, 60603.0, 60604.0, 60605.0, 60606.0, 60607.0, 60701.0, 60702.0, 60703.0, 60704.0, 60705.0, 60706.0, 60707.0, 70101.0, 70102.0, 70103.0, 70104.0, 70105.0, 70106.0, 70107.0, 70201.0, 70202.0, 70203.0, 70204.0, 70205.0, 70206.0, 70207.0, 70301.0, 70302.0, 70303.0, 70304.0, 70305.0, 70306.0, 70307.0, 70401.0, 70402.0, 70403.0, 70404.0, 70405.0, 70406.0, 70407.0, 70501.0, 70502.0, 70503.0, 70504.0, 70505.0, 70506.0, 70507.0, 70601.0, 70602.0, 70603.0, 70604.0, 70605.0, 70606.0, 70607.0, 70701.0, 70702.0, 70703.0, 70704.0, 70705.0, 70706.0, 70707.0] - }, - "new_lo": [1, 1, 1], - "new_hi": [8, 8, 8] - }, - { - "id": "clips_high", - "served": { - "shape": [1, 6, 6, 6], - "voxel": [8, 8, 8], - "translation": [4, 4, 4], - "chunks": [4, 4, 4] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [3, 2, 4], - "hi": [9, 5, 12], - "prediction": { - "dtype": "float32", - "shape": [1, 3, 3, 2], - "data": [30204.0, 30205.0, 30304.0, 30305.0, 30404.0, 30405.0, 40204.0, 40205.0, 40304.0, 40305.0, 40404.0, 40405.0, 50204.0, 50205.0, 50304.0, 50305.0, 50404.0, 50405.0] - }, - "new_lo": [3, 2, 4], - "new_hi": [6, 5, 6] - }, - { - "id": "anisotropic", - "served": { - "shape": [3, 6, 12, 12], - "voxel": [16, 8, 8], - "translation": [8, 4, 4], - "chunks": [3, 4, 4] - }, - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "lo": [1, 2, 3], - "hi": [5, 11, 9], - "prediction": { - "dtype": "float32", - "shape": [3, 4, 9, 6], - "data": [10203.0, 10204.0, 10205.0, 10206.0, 10207.0, 10208.0, 10303.0, 10304.0, 10305.0, 10306.0, 10307.0, 10308.0, 10403.0, 10404.0, 10405.0, 10406.0, 10407.0, 10408.0, 10503.0, 10504.0, 10505.0, 10506.0, 10507.0, 10508.0, 10603.0, 10604.0, 10605.0, 10606.0, 10607.0, 10608.0, 10703.0, 10704.0, 10705.0, 10706.0, 10707.0, 10708.0, 10803.0, 10804.0, 10805.0, 10806.0, 10807.0, 10808.0, 10903.0, 10904.0, 10905.0, 10906.0, 10907.0, 10908.0, 11003.0, 11004.0, 11005.0, 11006.0, 11007.0, 11008.0, 20203.0, 20204.0, 20205.0, 20206.0, 20207.0, 20208.0, 20303.0, 20304.0, 20305.0, 20306.0, 20307.0, 20308.0, 20403.0, 20404.0, 20405.0, 20406.0, 20407.0, 20408.0, 20503.0, 20504.0, 20505.0, 20506.0, 20507.0, 20508.0, 20603.0, 20604.0, 20605.0, 20606.0, 20607.0, 20608.0, 20703.0, 20704.0, 20705.0, 20706.0, 20707.0, 20708.0, 20803.0, 20804.0, 20805.0, 20806.0, 20807.0, 20808.0, 20903.0, 20904.0, 20905.0, 20906.0, 20907.0, 20908.0, 21003.0, 21004.0, 21005.0, 21006.0, 21007.0, 21008.0, 30203.0, 30204.0, 30205.0, 30206.0, 30207.0, 30208.0, 30303.0, 30304.0, 30305.0, 30306.0, 30307.0, 30308.0, 30403.0, 30404.0, 30405.0, 30406.0, 30407.0, 30408.0, 30503.0, 30504.0, 30505.0, 30506.0, 30507.0, 30508.0, 30603.0, 30604.0, 30605.0, 30606.0, 30607.0, 30608.0, 30703.0, 30704.0, 30705.0, 30706.0, 30707.0, 30708.0, 30803.0, 30804.0, 30805.0, 30806.0, 30807.0, 30808.0, 30903.0, 30904.0, 30905.0, 30906.0, 30907.0, 30908.0, 31003.0, 31004.0, 31005.0, 31006.0, 31007.0, 31008.0, 40203.0, 40204.0, 40205.0, 40206.0, 40207.0, 40208.0, 40303.0, 40304.0, 40305.0, 40306.0, 40307.0, 40308.0, 40403.0, 40404.0, 40405.0, 40406.0, 40407.0, 40408.0, 40503.0, 40504.0, 40505.0, 40506.0, 40507.0, 40508.0, 40603.0, 40604.0, 40605.0, 40606.0, 40607.0, 40608.0, 40703.0, 40704.0, 40705.0, 40706.0, 40707.0, 40708.0, 40803.0, 40804.0, 40805.0, 40806.0, 40807.0, 40808.0, 40903.0, 40904.0, 40905.0, 40906.0, 40907.0, 40908.0, 41003.0, 41004.0, 41005.0, 41006.0, 41007.0, 41008.0, 1010203.0, 1010204.0, 1010205.0, 1010206.0, 1010207.0, 1010208.0, 1010303.0, 1010304.0, 1010305.0, 1010306.0, 1010307.0, 1010308.0, 1010403.0, 1010404.0, 1010405.0, 1010406.0, 1010407.0, 1010408.0, 1010503.0, 1010504.0, 1010505.0, 1010506.0, 1010507.0, 1010508.0, 1010603.0, 1010604.0, 1010605.0, 1010606.0, 1010607.0, 1010608.0, 1010703.0, 1010704.0, 1010705.0, 1010706.0, 1010707.0, 1010708.0, 1010803.0, 1010804.0, 1010805.0, 1010806.0, 1010807.0, 1010808.0, 1010903.0, 1010904.0, 1010905.0, 1010906.0, 1010907.0, 1010908.0, 1011003.0, 1011004.0, 1011005.0, 1011006.0, 1011007.0, 1011008.0, 1020203.0, 1020204.0, 1020205.0, 1020206.0, 1020207.0, 1020208.0, 1020303.0, 1020304.0, 1020305.0, 1020306.0, 1020307.0, 1020308.0, 1020403.0, 1020404.0, 1020405.0, 1020406.0, 1020407.0, 1020408.0, 1020503.0, 1020504.0, 1020505.0, 1020506.0, 1020507.0, 1020508.0, 1020603.0, 1020604.0, 1020605.0, 1020606.0, 1020607.0, 1020608.0, 1020703.0, 1020704.0, 1020705.0, 1020706.0, 1020707.0, 1020708.0, 1020803.0, 1020804.0, 1020805.0, 1020806.0, 1020807.0, 1020808.0, 1020903.0, 1020904.0, 1020905.0, 1020906.0, 1020907.0, 1020908.0, 1021003.0, 1021004.0, 1021005.0, 1021006.0, 1021007.0, 1021008.0, 1030203.0, 1030204.0, 1030205.0, 1030206.0, 1030207.0, 1030208.0, 1030303.0, 1030304.0, 1030305.0, 1030306.0, 1030307.0, 1030308.0, 1030403.0, 1030404.0, 1030405.0, 1030406.0, 1030407.0, 1030408.0, 1030503.0, 1030504.0, 1030505.0, 1030506.0, 1030507.0, 1030508.0, 1030603.0, 1030604.0, 1030605.0, 1030606.0, 1030607.0, 1030608.0, 1030703.0, 1030704.0, 1030705.0, 1030706.0, 1030707.0, 1030708.0, 1030803.0, 1030804.0, 1030805.0, 1030806.0, 1030807.0, 1030808.0, 1030903.0, 1030904.0, 1030905.0, 1030906.0, 1030907.0, 1030908.0, 1031003.0, 1031004.0, 1031005.0, 1031006.0, 1031007.0, 1031008.0, 1040203.0, 1040204.0, 1040205.0, 1040206.0, 1040207.0, 1040208.0, 1040303.0, 1040304.0, 1040305.0, 1040306.0, 1040307.0, 1040308.0, 1040403.0, 1040404.0, 1040405.0, 1040406.0, 1040407.0, 1040408.0, 1040503.0, 1040504.0, 1040505.0, 1040506.0, 1040507.0, 1040508.0, 1040603.0, 1040604.0, 1040605.0, 1040606.0, 1040607.0, 1040608.0, 1040703.0, 1040704.0, 1040705.0, 1040706.0, 1040707.0, 1040708.0, 1040803.0, 1040804.0, 1040805.0, 1040806.0, 1040807.0, 1040808.0, 1040903.0, 1040904.0, 1040905.0, 1040906.0, 1040907.0, 1040908.0, 1041003.0, 1041004.0, 1041005.0, 1041006.0, 1041007.0, 1041008.0, 2010203.0, 2010204.0, 2010205.0, 2010206.0, 2010207.0, 2010208.0, 2010303.0, 2010304.0, 2010305.0, 2010306.0, 2010307.0, 2010308.0, 2010403.0, 2010404.0, 2010405.0, 2010406.0, 2010407.0, 2010408.0, 2010503.0, 2010504.0, 2010505.0, 2010506.0, 2010507.0, 2010508.0, 2010603.0, 2010604.0, 2010605.0, 2010606.0, 2010607.0, 2010608.0, 2010703.0, 2010704.0, 2010705.0, 2010706.0, 2010707.0, 2010708.0, 2010803.0, 2010804.0, 2010805.0, 2010806.0, 2010807.0, 2010808.0, 2010903.0, 2010904.0, 2010905.0, 2010906.0, 2010907.0, 2010908.0, 2011003.0, 2011004.0, 2011005.0, 2011006.0, 2011007.0, 2011008.0, 2020203.0, 2020204.0, 2020205.0, 2020206.0, 2020207.0, 2020208.0, 2020303.0, 2020304.0, 2020305.0, 2020306.0, 2020307.0, 2020308.0, 2020403.0, 2020404.0, 2020405.0, 2020406.0, 2020407.0, 2020408.0, 2020503.0, 2020504.0, 2020505.0, 2020506.0, 2020507.0, 2020508.0, 2020603.0, 2020604.0, 2020605.0, 2020606.0, 2020607.0, 2020608.0, 2020703.0, 2020704.0, 2020705.0, 2020706.0, 2020707.0, 2020708.0, 2020803.0, 2020804.0, 2020805.0, 2020806.0, 2020807.0, 2020808.0, 2020903.0, 2020904.0, 2020905.0, 2020906.0, 2020907.0, 2020908.0, 2021003.0, 2021004.0, 2021005.0, 2021006.0, 2021007.0, 2021008.0, 2030203.0, 2030204.0, 2030205.0, 2030206.0, 2030207.0, 2030208.0, 2030303.0, 2030304.0, 2030305.0, 2030306.0, 2030307.0, 2030308.0, 2030403.0, 2030404.0, 2030405.0, 2030406.0, 2030407.0, 2030408.0, 2030503.0, 2030504.0, 2030505.0, 2030506.0, 2030507.0, 2030508.0, 2030603.0, 2030604.0, 2030605.0, 2030606.0, 2030607.0, 2030608.0, 2030703.0, 2030704.0, 2030705.0, 2030706.0, 2030707.0, 2030708.0, 2030803.0, 2030804.0, 2030805.0, 2030806.0, 2030807.0, 2030808.0, 2030903.0, 2030904.0, 2030905.0, 2030906.0, 2030907.0, 2030908.0, 2031003.0, 2031004.0, 2031005.0, 2031006.0, 2031007.0, 2031008.0, 2040203.0, 2040204.0, 2040205.0, 2040206.0, 2040207.0, 2040208.0, 2040303.0, 2040304.0, 2040305.0, 2040306.0, 2040307.0, 2040308.0, 2040403.0, 2040404.0, 2040405.0, 2040406.0, 2040407.0, 2040408.0, 2040503.0, 2040504.0, 2040505.0, 2040506.0, 2040507.0, 2040508.0, 2040603.0, 2040604.0, 2040605.0, 2040606.0, 2040607.0, 2040608.0, 2040703.0, 2040704.0, 2040705.0, 2040706.0, 2040707.0, 2040708.0, 2040803.0, 2040804.0, 2040805.0, 2040806.0, 2040807.0, 2040808.0, 2040903.0, 2040904.0, 2040905.0, 2040906.0, 2040907.0, 2040908.0, 2041003.0, 2041004.0, 2041005.0, 2041006.0, 2041007.0, 2041008.0] - }, - "new_lo": [1, 2, 3], - "new_hi": [5, 11, 9] - }, - { - "id": "anisotropic_coarser_xy", - "served": { - "shape": [1, 6, 6, 6], - "voxel": [16, 16, 16], - "translation": [8, 8, 8], - "chunks": [3, 3, 3] - }, - "volume": { - "output_voxel_size": [16.0, 8.0, 8.0], - "dataset_offset_nm": [8.0, 4.0, 4.0] - }, - "lo": [0, 1, 1], - "hi": [5, 9, 10], - "prediction": { - "dtype": "float32", - "shape": [1, 5, 8, 9], - "data": [0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 100.0, 101.0, 101.0, 102.0, 102.0, 103.0, 103.0, 104.0, 104.0, 100.0, 101.0, 101.0, 102.0, 102.0, 103.0, 103.0, 104.0, 104.0, 200.0, 201.0, 201.0, 202.0, 202.0, 203.0, 203.0, 204.0, 204.0, 200.0, 201.0, 201.0, 202.0, 202.0, 203.0, 203.0, 204.0, 204.0, 300.0, 301.0, 301.0, 302.0, 302.0, 303.0, 303.0, 304.0, 304.0, 300.0, 301.0, 301.0, 302.0, 302.0, 303.0, 303.0, 304.0, 304.0, 400.0, 401.0, 401.0, 402.0, 402.0, 403.0, 403.0, 404.0, 404.0, 10000.0, 10001.0, 10001.0, 10002.0, 10002.0, 10003.0, 10003.0, 10004.0, 10004.0, 10100.0, 10101.0, 10101.0, 10102.0, 10102.0, 10103.0, 10103.0, 10104.0, 10104.0, 10100.0, 10101.0, 10101.0, 10102.0, 10102.0, 10103.0, 10103.0, 10104.0, 10104.0, 10200.0, 10201.0, 10201.0, 10202.0, 10202.0, 10203.0, 10203.0, 10204.0, 10204.0, 10200.0, 10201.0, 10201.0, 10202.0, 10202.0, 10203.0, 10203.0, 10204.0, 10204.0, 10300.0, 10301.0, 10301.0, 10302.0, 10302.0, 10303.0, 10303.0, 10304.0, 10304.0, 10300.0, 10301.0, 10301.0, 10302.0, 10302.0, 10303.0, 10303.0, 10304.0, 10304.0, 10400.0, 10401.0, 10401.0, 10402.0, 10402.0, 10403.0, 10403.0, 10404.0, 10404.0, 20000.0, 20001.0, 20001.0, 20002.0, 20002.0, 20003.0, 20003.0, 20004.0, 20004.0, 20100.0, 20101.0, 20101.0, 20102.0, 20102.0, 20103.0, 20103.0, 20104.0, 20104.0, 20100.0, 20101.0, 20101.0, 20102.0, 20102.0, 20103.0, 20103.0, 20104.0, 20104.0, 20200.0, 20201.0, 20201.0, 20202.0, 20202.0, 20203.0, 20203.0, 20204.0, 20204.0, 20200.0, 20201.0, 20201.0, 20202.0, 20202.0, 20203.0, 20203.0, 20204.0, 20204.0, 20300.0, 20301.0, 20301.0, 20302.0, 20302.0, 20303.0, 20303.0, 20304.0, 20304.0, 20300.0, 20301.0, 20301.0, 20302.0, 20302.0, 20303.0, 20303.0, 20304.0, 20304.0, 20400.0, 20401.0, 20401.0, 20402.0, 20402.0, 20403.0, 20403.0, 20404.0, 20404.0, 30000.0, 30001.0, 30001.0, 30002.0, 30002.0, 30003.0, 30003.0, 30004.0, 30004.0, 30100.0, 30101.0, 30101.0, 30102.0, 30102.0, 30103.0, 30103.0, 30104.0, 30104.0, 30100.0, 30101.0, 30101.0, 30102.0, 30102.0, 30103.0, 30103.0, 30104.0, 30104.0, 30200.0, 30201.0, 30201.0, 30202.0, 30202.0, 30203.0, 30203.0, 30204.0, 30204.0, 30200.0, 30201.0, 30201.0, 30202.0, 30202.0, 30203.0, 30203.0, 30204.0, 30204.0, 30300.0, 30301.0, 30301.0, 30302.0, 30302.0, 30303.0, 30303.0, 30304.0, 30304.0, 30300.0, 30301.0, 30301.0, 30302.0, 30302.0, 30303.0, 30303.0, 30304.0, 30304.0, 30400.0, 30401.0, 30401.0, 30402.0, 30402.0, 30403.0, 30403.0, 30404.0, 30404.0, 40000.0, 40001.0, 40001.0, 40002.0, 40002.0, 40003.0, 40003.0, 40004.0, 40004.0, 40100.0, 40101.0, 40101.0, 40102.0, 40102.0, 40103.0, 40103.0, 40104.0, 40104.0, 40100.0, 40101.0, 40101.0, 40102.0, 40102.0, 40103.0, 40103.0, 40104.0, 40104.0, 40200.0, 40201.0, 40201.0, 40202.0, 40202.0, 40203.0, 40203.0, 40204.0, 40204.0, 40200.0, 40201.0, 40201.0, 40202.0, 40202.0, 40203.0, 40203.0, 40204.0, 40204.0, 40300.0, 40301.0, 40301.0, 40302.0, 40302.0, 40303.0, 40303.0, 40304.0, 40304.0, 40300.0, 40301.0, 40301.0, 40302.0, 40302.0, 40303.0, 40303.0, 40304.0, 40304.0, 40400.0, 40401.0, 40401.0, 40402.0, 40402.0, 40403.0, 40403.0, 40404.0, 40404.0] - }, - "new_lo": [0, 1, 1], - "new_hi": [5, 9, 10] - }, - { - "id": "outside", - "served": { - "shape": [1, 4, 4, 4], - "voxel": [8, 8, 8], - "translation": [4, 4, 4], - "chunks": [4, 4, 4] - }, - "volume": { - "output_voxel_size": [8.0, 8.0, 8.0], - "dataset_offset_nm": [4.0, 4.0, 4.0] - }, - "lo": [5, 0, 0], - "hi": [8, 4, 4], - "error": { - "message": "The view is outside what the model's server predicts.", - "status": 400 - } + "count": 1 } ] } diff --git a/web/test/fixtures/make_ai_fixtures.py b/web/test/fixtures/make_ai_fixtures.py deleted file mode 100644 index ed6012ea..00000000 --- a/web/test/fixtures/make_ai_fixtures.py +++ /dev/null @@ -1,687 +0,0 @@ -"""Generate the parity fixture for the browser's AI-assisted annotation core. - -The browser mode re-implements the server's AI annotation geometry and -labelling in TypeScript (``web/src/ai-annotate/``: ``geometry.ts`` from -``cellmap_flow/ai_annotate/geometry.py``, ``plane.ts`` from ``pipeline.py``, -``mask.ts`` from ``mask_extraction.py``, ``labels.ts`` and ``fill.ts`` from -``finetune/session/fill.py``, ``prompts.ts`` and ``organelles.ts`` from the -modules of the same names). A plane planned in the browser must be the plane -the server would plan for the same volume and point, the mask must land on -the same voxels and get the same ids, and the model must be asked with the -same words, so the vitest suite in ``web/test/parity/ai-annotate.test.ts`` -checks each port against what the *Python functions themselves* produce, -written here as JSON, rather than against a reading of the Python code. - -A separate generator from ``make_fixtures.py`` (whose helpers and conventions -it shares): the cases are plans, planes and boxes, not blocks through a -chain. Run it with a Python that has cellmap_flow's dependencies (numpy, -scipy, PIL, zarr), from anywhere:: - - .pixi/envs/default/bin/python web/test/fixtures/make_ai_fixtures.py # regenerate - .pixi/envs/default/bin/python web/test/fixtures/make_ai_fixtures.py --check # CI: stale? - -``--check`` regenerates into a temporary directory and compares with the -committed file (floats with a small tolerance, everything else exactly), -exiting 1 when the Python has changed and the fixture has not; -``tests/web/test_fixtures_fresh.py`` runs that in pytest. - -Everything is deterministic (a fixed seed, recorded in the file) and the -script is idempotent. Arrays are ``{"dtype", "shape", "data"}`` with ``data`` -flat in C order (``make_fixtures.array_record``). The sections of -``ai_annotate.json``: - -``screen`` - ``to_screen``/``from_screen``/``screen_shape`` of a small plane per depth axis, - for a viewer that sets no display dimensions and for cellmap-flow's x, y, z. -``plan_plane`` - ``plan_plane`` cases (volumes, shapes, points, depth axes, crop sizes, - data boxes): the plan ``to_dict`` wrote with its ``click_px``, - ``plane_axes``, ``write_shape`` and ``plane_name``, or the refusal's - category, message and status. -``raw_box`` - ``raw_box`` of some of those plans on raw grids of several voxel sizes and corners. -``extract_mask`` - one small RGB image and the mask ``extract_mask`` finds for each catalog colour. -``normalize_to_uint8`` - ``normalize_to_uint8`` of a float32, a uint16 and a uint8 plane. -``resize`` - ``pipeline._resize`` (PIL BOX when shrinking, BILINEAR otherwise) of - small float32 planes, float32 results. -``mask_to_write_shape`` - random masks of a plan's image cut into objects on its write box, and - two objects parted by a one-pixel line on voxels coarser than the pixels. -``box_voxels``, ``fresh_ids``, ``seed_labels``, ``labels_for_box`` - the box arithmetic and the ids, incl. refusals' messages. -``paint_box`` - ``fill.paint_box`` and ``fill.restore_box`` on an 8^3 volume in 4^3 - chunks, MinIO replaced by an in-memory zarr as tests/ai_annotate/ - test_paint_box.py replaces it: the volume before, the labels written, - the counts, the volume after, the undo entry, and a restore after a - stroke painted in between. -``prompts``, ``organelles`` - the prompts word for word, and the catalog with its lookups. -""" - -from __future__ import annotations - -import argparse -import json -import sys -import tempfile -from pathlib import Path - -import numpy as np -import zarr -from scipy.ndimage import label - -HERE = Path(__file__).resolve().parent -sys.path.insert(0, str(HERE)) - -from make_fixtures import ROOT, array_record, differences, fixture_text # noqa: E402 (puts the worktree's package first on sys.path) - -from cellmap_flow.ai_annotate import geometry, mask_extraction, organelles, pipeline, prompts # noqa: E402 -from cellmap_flow.ai_annotate.errors import AIAnnotateError # noqa: E402 -from cellmap_flow.finetune.session import fill, minio # noqa: E402 - -FIXTURES = ("ai_annotate.json",) -SEED = 20261015 - - -def _error(e: AIAnnotateError) -> dict: - return {"category": e.category, "message": e.user_message, "http_status": e.http_status} - - -def _volume(output=(8, 8, 8), input=(4, 4, 4), offset=None) -> dict: - """An annotation volume's record: voxel 0's corner at the origin unless ``offset`` (its centre) is given.""" - output = np.asarray(output, dtype=float) - return { - "output_voxel_size": [float(v) for v in output], - "input_voxel_size": [float(v) for v in input], - "dataset_offset_nm": [float(v) for v in (output / 2 if offset is None else np.asarray(offset, dtype=float))], - } - - -SHAPE = (100, 200, 300) -V8 = _volume() -ANISO = _volume(output=(16, 8, 8), input=(8, 4, 4)) -CENTRE = (50 * 8 + 1, 100 * 8 + 3, 150 * 8 + 6) # in voxel (50, 100, 150), off its centre - - -# --- screen layout -------------------------------------------------------------- - -# A viewer that sets no display dimensions (z, y, x) and cellmap-flow's own. -DISPLAYS = (None, ("x", "y", "z")) - - -def make_screen() -> list: - plane = np.arange(6, dtype=np.int32).reshape(2, 3) # rows 2, cols 3, in z, y, x order - image = np.arange(12, dtype=np.int32).reshape(3, 4) # as a panel would draw it - cases = [] - for display in DISPLAYS: - for depth_axis in (0, 1, 2): - cases.append({ - "depth_axis": depth_axis, - "display_dimensions": list(display) if display else None, - "plane": array_record(plane), - "to_screen": array_record(geometry.to_screen(plane, depth_axis, display)), - "screen_shape": list(geometry.screen_shape(plane.shape, depth_axis, display)), - "image": array_record(image), - "from_screen": array_record(geometry.from_screen(image, depth_axis, display)), - }) - return cases - - -# --- plan_plane and raw_box ----------------------------------------------------- - -# id, volume, volume_shape, point, depth_axis, crop_size_px, data_box_nm -PLAN_CASES = [ - ("centred_z", V8, SHAPE, CENTRE, 0, 64, None), - ("centred_y", V8, SHAPE, CENTRE, 1, 64, None), - ("centred_x", V8, SHAPE, CENTRE, 2, 64, None), - # A point on a voxel boundary takes the voxel above. - ("boundary", V8, SHAPE, (80, 800, 1200), 0, 8, None), - # dataset_offset_nm 100: voxel 0 covers [96, 104) on every axis. - ("offset_100", _volume(offset=(100, 100, 100)), SHAPE, (103, 104 + 8 * 20, 104 + 8 * 30), 0, 4, None), - # 16 nm z, 8 nm y and x, the input at half each: a square field, square pixels. - ("aniso_xz", ANISO, SHAPE, (50 * 16 + 8, 100 * 8 + 4, 150 * 8 + 4), 1, 64, None), - ("aniso_xy", ANISO, SHAPE, (50 * 16 + 8, 100 * 8 + 4, 150 * 8 + 4), 0, 64, None), - ("aniso_yz", ANISO, SHAPE, (50 * 16 + 8, 100 * 8 + 4, 150 * 8 + 4), 2, 64, None), - # 512 annotation voxels of 8 nm read at 4 nm: 1024 px; read at 8 nm: 512. - ("io_differ_4nm", V8, (100, 2000, 2000), (400, 8000, 8000), 0, 512, None), - ("io_same_8nm", _volume(input=(8, 8, 8)), (100, 2000, 2000), (400, 8000, 8000), 0, 512, None), - # Longer than 1024 px at the input voxel size: sent at larger pixels. - ("downsampled", V8, (100, 2000, 2000), (400, 8000, 8000), 0, 1024, None), - ("downsampled_narrow", V8, (100, 2000, 2000), (400, 8000, 40), 0, 1024, None), - # 1001 voxels across at 16 nm pixels is 500.5 px: numpy rounds halves to even. - ("half_pixel_rounding", V8, (100, 3000, 1001), (400, 12000, 4000), 0, 2048, None), - # Voxel (50, 3, 297): 3 voxels from y's start, 2 from x's end. - ("clipped_edge", V8, SHAPE, (50 * 8 + 4, 3 * 8 + 4, 297 * 8 + 4), 0, 64, None), - # The data ends at x = 1210 nm while the volume runs to 2400: the field stops there. - ("data_box_clip", V8, SHAPE, CENTRE, 0, 64, ((0, 0, 0), (800, 1600, 1210))), - ("data_box_refused", V8, SHAPE, CENTRE, 0, 64, ((0, 0, 0), (800, 1600, 1200))), - # Outside the volume. - ("outside_before_z", V8, SHAPE, (-1, 800, 1200), 0, 64, None), - ("outside_past_x", V8, SHAPE, (400, 800, 300 * 8), 0, 64, None), - ("outside_fraction", V8, SHAPE, (400, -0.5, 1200), 0, 64, None), - ("outside_at_end_z", V8, SHAPE, (100 * 8 + 4, 800, 1200), 0, 64, None), - # Small fields for the mask cases: 16 voxels, 32 px. - ("small_z", V8, SHAPE, CENTRE, 0, 16, None), - ("small_y", V8, SHAPE, CENTRE, 1, 16, None), - ("small_x", V8, SHAPE, CENTRE, 2, 16, None), - ("small_aniso_xz", ANISO, SHAPE, (50 * 16 + 8, 100 * 8 + 4, 150 * 8 + 4), 1, 16, None), - ("small_aniso_yz", ANISO, SHAPE, (50 * 16 + 8, 100 * 8 + 4, 150 * 8 + 4), 2, 16, None), - ("small_clipped", V8, SHAPE, (50 * 8 + 4, 3 * 8 + 4, 297 * 8 + 4), 0, 16, None), - ("small_same", _volume(input=(8, 8, 8)), SHAPE, CENTRE, 0, 16, None), - ("small_offset_100", _volume(offset=(100, 100, 100)), SHAPE, (103, 104 + 8 * 20, 104 + 8 * 30), 1, 12, None), -] - -MASK_PLAN_IDS = ("small_z", "small_y", "small_x", "small_aniso_xz", "small_aniso_yz", "small_clipped", "small_same", "small_offset_100") - -# plan id, raw voxel size, raw corner -RAW_BOX_CASES = [ - ("centred_z", (4, 4, 4), (0, 0, 0)), - ("centred_z", (4, 4, 4), (-1, -1, -1)), - ("centred_z", (4, 4, 4), (1, 1, 1)), - ("centred_z", (4, 4, 4), (3, 3, 3)), - ("centred_z", (8, 8, 8), (0, 0, 0)), - ("centred_y", (4, 4, 4), (0, 0, 0)), - ("aniso_xz", (8, 4, 4), (0, 0, 0)), - ("aniso_xz", (8, 4, 4), (-4, -2, -2)), - ("downsampled", (4, 4, 4), (0, 0, 0)), - ("small_offset_100", (4, 4, 4), (96, 96, 96)), -] - - -# The cases planned again for a viewer showing x, y, z, as cellmap-flow's do: -# the same boxes, the image laid out as that viewer's panels draw it. -SHOWN_XYZ = ("centred_z", "centred_y", "centred_x", "aniso_xz") - - -def make_plans() -> tuple[list, dict]: - cases, plans = [], {} - shown = [(f"{c[0]}_shown_xyz", *c[1:], ("x", "y", "z")) for c in PLAN_CASES if c[0] in SHOWN_XYZ] - for case_id, volume, shape, point, depth_axis, crop, data_box, *display in [*PLAN_CASES, *shown]: - display = display[0] if display else None - case = { - "id": case_id, "volume": volume, "volume_shape": list(shape), "point_nm": [float(p) for p in point], - "depth_axis": depth_axis, "crop_size_px": crop, - "data_box_nm": [[float(v) for v in side] for side in data_box] if data_box else None, - "display_dimensions": list(display) if display else None, - } - try: - plan = geometry.plan_plane(volume, shape, point, depth_axis, crop, data_box_nm=data_box, - display_dimensions=display) - except AIAnnotateError as e: - case["error"] = _error(e) - else: - plans[case_id] = plan - case.update({ - "plan": plan.to_dict(), "click_px": list(plan.click_px), "plane_axes": list(plan.plane_axes), - "write_shape": list(plan.write_shape), "plane_name": plan.plane_name, - }) - cases.append(case) - return cases, plans - - -def make_raw_boxes(plans: dict) -> list: - cases = [] - for plan_id, voxel, corner in RAW_BOX_CASES: - box = geometry.raw_box(plans[plan_id], voxel, corner) - cases.append({"plan": plan_id, "voxel_size": list(voxel), "corner": list(corner), "begin": list(box.begin), "count": list(box.shape)}) - return cases - - -# --- masks and planes ----------------------------------------------------------- - - -def make_extract_mask() -> dict: - pixels = [ - [(255, 0, 0), (230, 20, 10), (200, 0, 0), (255, 128, 0), (255, 100, 0), (128, 0, 255)], - [(120, 10, 250), (255, 255, 255), (0, 0, 0), (255, 255, 0), (0, 255, 0), (0, 255, 128)], - [(0, 0, 255), (0, 255, 255), (255, 0, 255), (255, 0, 128), (60, 60, 60), (254, 1, 1)], - ] - image = np.asarray(pixels, dtype=np.uint8) - targets = {} - for profile in organelles.ORGANELLES.values(): - targets.setdefault(profile.color_name, tuple(int(c) for c in profile.rgb)) - cases = [] - for name, rgb in sorted(targets.items()): - for threshold in (200.0, 100.0): - mask = mask_extraction.extract_mask(image, rgb, threshold=threshold) - cases.append({"color_name": name, "target_rgb": list(rgb), "threshold": threshold, "mask": array_record(mask.astype(np.uint8))}) - return {"image": array_record(image), "cases": cases} - - -def make_normalize(rng: np.random.Generator) -> list: - floats = (rng.standard_normal((6, 7)) * 1000).astype(np.float32) - floats[0, 0], floats[5, 6] = 50000.0, -50000.0 # outliers the percentiles clip - uint16 = rng.integers(1000, 40000, size=(5, 8), dtype=np.uint16) - uint8 = rng.integers(0, 256, size=(4, 5), dtype=np.uint8) - cases = [] - for name, plane in (("float32", floats), ("uint16", uint16), ("uint8", uint8)): - cases.append({"id": name, "plane": array_record(plane), "uint8": array_record(mask_extraction.normalize_to_uint8(plane))}) - return cases - - -RESIZE_CASES = [ - ("shrink_whole", (6, 8), (3, 4)), - ("shrink_fraction", (7, 5), (3, 2)), - ("grow_whole", (3, 4), (6, 8)), - ("grow_fraction", (3, 3), (7, 5)), - ("mixed", (6, 3), (3, 6)), - ("same", (5, 5), (5, 5)), - ("rows_only", (8, 8), (5, 8)), - ("cols_only", (4, 9), (4, 3)), - ("one_row", (1, 6), (1, 4)), -] - - -def make_resize(rng: np.random.Generator) -> list: - cases = [] - for case_id, in_shape, out_shape in RESIZE_CASES: - plane = (rng.random(in_shape) * 255).astype(np.float32) - out = pipeline._resize(plane, out_shape) - cases.append({"id": case_id, "plane": array_record(plane), "shape": list(out_shape), "resized": array_record(np.asarray(out, dtype=np.float32))}) - return cases - - -def make_mask_to_write_shape(rng: np.random.Generator, plans: dict) -> list: - masks = [(plan_id, plan_id, rng.random(tuple(plans[plan_id].image_shape)) < 0.5) for plan_id in MASK_PLAN_IDS] - # 32 px on 16 voxels: each voxel takes an odd pixel column, none the - # line in column 16 between the two objects. - line = np.ones(tuple(plans["small_z"].image_shape), dtype=bool) - line[:, 16] = False - masks.append(("small_z_line", "small_z", line)) - cases = [] - for case_id, plan_id, mask in masks: - out = pipeline.mask_to_write_shape(mask, plans[plan_id]) - cases.append({"id": case_id, "plan": plan_id, "mask": array_record(mask.astype(np.uint8)), "write": array_record(out)}) - return cases - - -# --- boxes and ids -------------------------------------------------------------- - - -def make_box_voxels() -> list: - cases = [] - for case_id, volume, offset, size, shape in [ - ("on_grid", V8, (32, 32, 32), (8, 16, 24), SHAPE), - ("centres_decide", V8, (30, 36, 44), (12, 8, 8), SHAPE), - ("clipped", V8, (-20, 1580, 2380), (40, 40, 40), SHAPE), - ("outside", V8, (-100, 0, 0), (40, 40, 40), SHAPE), - ("offset_100", _volume(offset=(100, 100, 100)), (96, 100, 104), (8, 8, 8), SHAPE), - ("aniso", ANISO, (0, 0, 0), (33, 17, 15), SHAPE), - ]: - box = fill.box_voxels(volume, offset, size, shape) - cases.append({ - "id": case_id, "volume": volume, "offset_nm": list(offset), "shape_nm": list(size), "volume_shape": list(shape), - "corner_nm": [float(v) for v in fill.volume_corner_nm(volume["dataset_offset_nm"], volume["output_voxel_size"])], - "box": None if box is None else {"lo": [int(v) for v in box[0]], "hi": [int(v) for v in box[1]]}, - }) - return cases - - -def make_fresh_ids() -> list: - full = np.arange(2, 256, dtype=np.uint8) # every uint8 id in use - cases = [] - for case_id, existing, count, count_up in [ - ("lowest_free", np.array([0, 0, 1, 2, 3, 5], dtype=np.uint8), 3, False), - ("count_up", np.array([0, 0, 1, 2, 3, 5], dtype=np.uint8), 3, True), - ("count_up_from_1", np.zeros(4, dtype=np.uint8), 2, True), - ("uint16_count_up", np.array([0, 300, 7], dtype=np.uint16), 2, True), - ("uint16_lowest_free", np.array([0, 300, 7], dtype=np.uint16), 2, False), - ("none", np.array([0, 2], dtype=np.uint8), 0, False), - ("none_count_up", np.array([0, 2], dtype=np.uint8), 0, True), - ("uint8_full", full, 1, False), - ("uint8_last_free", full[:-1], 1, False), - ("uint8_count_up_overflow", np.array([255], dtype=np.uint8), 1, True), - ("uint16_count_up_overflow", np.array([65534], dtype=np.uint16), 2, True), - ]: - case = {"id": case_id, "existing": array_record(existing), "count": count, "count_up": count_up} - try: - case["ids"] = [int(v) for v in fill._fresh_ids(existing, count, count_up)] - except ValueError as e: - case["error"] = str(e) - cases.append(case) - return cases - - -def make_seed_labels() -> list: - def objects_of(shape, *blocks): - objects = np.zeros(shape, dtype=np.int32) - for value, sl in blocks: - objects[sl] = value - return objects - - two = objects_of((4, 6), (1, (slice(0, 2), slice(0, 2))), (2, (slice(2, 4), slice(3, 6)))) - cases = [] - for case_id, objects, existing, count_up in [ - ("fresh", two, np.zeros((4, 6), dtype=np.uint8), False), - ("fresh_count_up", two, np.zeros((4, 6), dtype=np.uint8), True), - ("painted_kept", two, objects_of((4, 6), (7, (0, 0)), (9, (3, 0))).astype(np.uint8), False), - ("tie_takes_larger_id", two, objects_of((4, 6), (4, (0, slice(0, 1))), (6, (1, slice(0, 1))), (3, (3, 5))).astype(np.uint8), False), - ("majority", two, objects_of((4, 6), (4, (0, slice(0, 2))), (6, (1, slice(0, 1)))).astype(np.uint8), False), - ("skipped_object_id", objects_of((2, 5), (1, (0, 0)), (3, (1, 4))), np.zeros((2, 5), dtype=np.uint8), False), - ("uint16_count_up", two, objects_of((4, 6), (300, (0, 5))).astype(np.uint16), True), - ("same_id_two_objects", two, objects_of((4, 6), (5, (0, 0)), (5, (3, 5))).astype(np.uint8), False), - ("no_foreground", np.zeros((3, 3), dtype=np.int32), objects_of((3, 3), (4, (1, 1))).astype(np.uint8), False), - ("background_painted_ignored", two, objects_of((4, 6), (1, (0, 0)), (1, (2, 3))).astype(np.uint8), False), - ]: - case = {"id": case_id, "objects": array_record(objects), "existing": array_record(existing), "count_up": count_up} - try: - case["labels"] = array_record(fill.seed_labels(objects, existing, count_up=count_up)) - except ValueError as e: - case["error"] = str(e) - cases.append(case) - return cases - - -def make_labels_for_box() -> list: - def box(plane, depth_axis): - return np.expand_dims(np.asarray(plane), depth_axis) - - cases = [] - - # Each object an id; a pixel touching only at a corner is its own object. - mask = np.zeros((6, 8), dtype=bool) - mask[0:2, 0:2] = True - mask[3:5, 4:7] = True - mask[5, 7] = True - for depth_axis in (0, 1, 2): - cases.append(("each_object_an_id", mask, box(np.zeros((6, 8), dtype=np.uint8), depth_axis), depth_axis, False, None)) - - # A painted object keeps its id. - mask = np.zeros((6, 6), dtype=bool) - mask[1:4, 1:4] = True - existing = np.zeros((6, 6), dtype=np.uint8) - existing[1, 1] = 7 - existing[5, 5] = 9 - cases.append(("painted_kept", mask, box(existing, 0), 0, False, None)) - - # Counting up on an instance volume. - mask = np.zeros((4, 8), dtype=bool) - mask[:, 0:2] = True - mask[:, 4:6] = True - existing = np.zeros((4, 8), dtype=np.uint16) - existing[0, 7] = 300 - cases.append(("count_up", mask, box(existing, 1), 1, True, None)) - - # An object continuing one in the plane before keeps its id; a new one skips the ids beside. - before = np.ones((6, 12), dtype=np.uint16) - before[1:4, 0:3] = 2 - before[1:4, 5:8] = 3 - mask = np.zeros((6, 12), dtype=bool) - mask[1:4, 5:8] = True - mask[1:4, 9:12] = True - for depth_axis in (0, 1, 2): - cases.append(("continues_beside", mask, box(np.zeros((6, 12), dtype=np.uint16), depth_axis), depth_axis, True, - [box(before, depth_axis), None])) - - # A new object on a uint8 volume skips the ids in the plane after. - after = np.zeros((4, 8), dtype=np.uint8) - after[:, 0:2] = 2 - mask = np.zeros((4, 8), dtype=bool) - mask[:, 5:7] = True - cases.append(("skips_ids_beside", mask, box(np.zeros((4, 8), dtype=np.uint8), 0), 0, False, [None, box(after, 0)])) - - # Both planes beside: the first given wins where both have an id. - first = np.zeros((4, 8), dtype=np.uint8) - first[1:3, 1:3] = 5 - second = np.zeros((4, 8), dtype=np.uint8) - second[1:3, 1:3] = 6 - second[1:3, 5:7] = 8 - mask = np.zeros((4, 8), dtype=bool) - mask[1:3, 1:3] = True - mask[1:3, 5:7] = True - cases.append(("first_plane_wins", mask, box(np.zeros((4, 8), dtype=np.uint8), 2), 2, False, [box(first, 2), box(second, 2)])) - - out = [] - for case_id, mask, existing, depth_axis, count_up, beside in cases: - objects, _ = label(mask) - labels = pipeline.labels_for_box(objects, existing, depth_axis, count_up=count_up, beside=beside) - out.append({ - "id": f"{case_id}_axis{depth_axis}", "objects": array_record(objects), "existing": array_record(existing), - "depth_axis": depth_axis, "count_up": count_up, - "beside": None if beside is None else [None if p is None else array_record(p) for p in beside], - "labels": array_record(labels), - }) - return out - - -# --- paint_box and restore_box -------------------------------------------------- - -STATE = {"bucket": "annotations"} - - -class _Bucket: - """MinIO's bucket as s3fs sees it: an in-memory zarr per key (tests/ai_annotate/test_paint_box.py).""" - - def __init__(self): - self.stores = {} - - def _split(self, path): - root, _, rest = path.partition(".zarr/") - return self.stores.get(root + ".zarr", {}), rest - - def exists(self, path): - store, rest = self._split(path) - return rest in store - - -def _served(initial: np.ndarray, chunks=(4, 4, 4)): - """``initial`` as MinIO would serve it: fill's S3 calls answered by an in-memory zarr.""" - bucket = _Bucket() - store = bucket.stores["annotations/vol-1.zarr"] = zarr.MemoryStore() - minio.make_s3_filesystem = lambda state: bucket - fill.s3fs.S3Map = lambda root, s3, check: s3.stores[root] - labels = zarr.open_group(store).require_group("annotation").create_dataset( - "s0", shape=initial.shape, chunks=chunks, dtype=initial.dtype, fill_value=0 - ) - labels[:] = initial - return labels - - -def _painted_corner(dtype) -> np.ndarray: - """8^3 of 0s with background (1) and an object (5) painted in the first slice of the first chunk.""" - painted = np.zeros((8,) * 3, dtype=dtype) - painted[0, 0, :2] = 1 - painted[0, 1, :2] = 5 - return painted - - -def _seed(existing): - """An object (2) over the box's first two rows of y, background (1) elsewhere.""" - labels = np.ones_like(existing) - labels[:, :2, :] = 2 - return labels - - -def make_paint_box() -> list: - across = _painted_corner(np.uint8) - across[2, 3, 4] = 1 # inside the across-chunks box: kept without overwrite - across[4, 5, 6] = 7 - - def object_3(existing): - labels = np.ones_like(existing) - labels[1:3, 1:3, 1:3] = 3 - return labels - - def nothing(existing): - return np.zeros_like(existing) - - uint16 = _painted_corner(np.uint16) - uint16[0, 1, :2] = 300 - - cases = [] - for case_id, initial, lo, hi, labels_for, overwrite, stroke in [ - ("seed", _painted_corner(np.uint8), (0, 0, 0), (4, 4, 4), _seed, False, [((0, 2, 0), 9), ((0, 0, 3), 9), ((6, 6, 6), 9)]), - ("seed_overwrite", _painted_corner(np.uint8), (0, 0, 0), (4, 4, 4), _seed, True, [((0, 1, 0), 9)]), - ("across_chunks", across, (1, 2, 3), (5, 6, 7), object_3, False, [((2, 2, 2), 1)]), - ("across_chunks_overwrite", across, (1, 2, 3), (5, 6, 7), object_3, True, None), - ("nothing_changes", _painted_corner(np.uint8), (0, 0, 0), (4, 4, 4), nothing, False, None), - ("unchanged_labels", _painted_corner(np.uint8), (0, 0, 0), (4, 4, 4), lambda existing: existing.copy(), True, None), - ("uint16", uint16, (0, 0, 0), (4, 4, 8), _seed, False, [((0, 3, 7), 400)]), - ]: - served = _served(initial) - lo_arr, hi_arr = np.asarray(lo), np.asarray(hi) - box = tuple(slice(int(a), int(b)) for a, b in zip(lo, hi)) - labels = labels_for(initial[box]) - undo = [] - counts = fill.paint_box(STATE, "vol-1", lo_arr, hi_arr, labels_for, undo=undo, overwrite=overwrite) - after = served[:] - case = { - "id": case_id, "dtype": str(initial.dtype), "shape": list(initial.shape), "chunks": [4, 4, 4], - "initial": array_record(initial), "lo": list(lo), "hi": list(hi), "labels": array_record(labels), "overwrite": overwrite, - "counts": {"n_foreground": counts[0], "n_background": counts[1], "n_overwritten": counts[2]}, - "after": array_record(after), - "undo": None if not undo else {"before": array_record(undo[0][2]), "after": array_record(undo[0][3])}, - } - if stroke is not None and undo: - # A stroke painted after the action: restore puts back only what - # the action changed and still holds. - for index, value in stroke: - served[tuple(index)] = value - restored = fill.restore_box(STATE, "vol-1", lo_arr, hi_arr, undo[0][2], undo[0][3]) - case["restore"] = { - "stroke": [[list(index), int(value)] for index, value in stroke], - "restored": int(restored), - "final": array_record(served[:]), - } - cases.append(case) - return cases - - -# --- prompts and the catalog ---------------------------------------------------- - - -def _profile_dict(profile) -> dict: - return {"key": profile.key, "name": profile.name, "color_name": profile.color_name, "rgb": list(profile.rgb), "description": profile.description} - - -def make_prompts() -> dict: - mito, er, cell, nucleus = (organelles.ORGANELLES[k] for k in ("mito", "er", "cell", "nucleus")) - golgi = organelles.resolve_organelle_profile("golgi") - cases = [] - for case_id, profile, resolution, override in [ - ("mito_8", mito, 8.0, None), - ("mito_3_5", mito, 3.5, None), - ("mito_4", mito, 4.0, None), - ("er_12_3456", er, 12.3456, None), - ("cell_1024", cell, 1024.0, None), - ("nucleus_none", nucleus, None, None), - ("override", mito, 8.0, " Paint the dark blobs red. "), - ("blank_override", mito, 8.0, " "), - ("generic", golgi, 16.0, None), - ("generic_override", golgi, 0.5, "Colour every Golgi stack bright red."), - ("too_long", mito, 8.0, "x" * (prompts.MAX_PROMPT_CHARS + 1)), - ("just_fits", mito, 8.0, "y" * prompts.MAX_PROMPT_CHARS), - ]: - case = {"id": case_id, "profile": _profile_dict(profile), "resolution_nm": resolution, "prompt_override": override} - try: - case["prompt"] = prompts.build_recolor_prompt(profile, resolution_nm=resolution, prompt_override=override) - except AIAnnotateError as e: - case["error"] = _error(e) - cases.append(case) - return { - "max_prompt_chars": prompts.MAX_PROMPT_CHARS, - "editable": {key: prompts.editable_prompt(profile) for key, profile in organelles.ORGANELLES.items()}, - "editable_generic": prompts.editable_prompt(golgi), - "cases": cases, - } - - -def make_organelles() -> dict: - lookups = [] - for name in ["mito", "Mitochondria", " nuclear-pores ", "lipid_droplet", "Cell Membrane", "ENDOPLASMIC RETICULUM", "nuclear_pore", - "Nucleolus", "cells", "cell", "Lipid Droplet", "golgi", "", "mito-chondria", "plasma-membrane"]: - profile = organelles.find_organelle_profile(name) - lookups.append({"name": name, "key": None if profile is None else profile.key}) - return { - "catalog": [_profile_dict(p) for p in organelles.ORGANELLES.values()], - "lookups": lookups, - "resolved": [{"name": name, "profile": _profile_dict(organelles.resolve_organelle_profile(name))} for name in ("golgi", " golgi apparatus ", "Mito")], - } - - -# --- writing and checking ------------------------------------------------------- - - -def make_ai_annotate() -> dict: - rng = np.random.default_rng(SEED) - plans, plan_objects = make_plans() - return { - "_format": ( - "Parity fixture for web/src/ai-annotate (geometry, plane, mask, labels, fill, prompts, organelles) " - "against cellmap_flow/ai_annotate and finetune/session/fill.py; arrays are {dtype, shape, data} flat in C order; " - "see make_ai_fixtures.py's docstring for the sections" - ), - "seed": SEED, - "constants": {"max_image_side": geometry.MAX_IMAGE_SIDE, "plane_names": list(geometry.PLANE_NAMES)}, - "screen": make_screen(), - "plan_plane": plans, - "raw_box": make_raw_boxes(plan_objects), - "extract_mask": make_extract_mask(), - "normalize_to_uint8": make_normalize(rng), - "resize": make_resize(rng), - "mask_to_write_shape": make_mask_to_write_shape(rng, plan_objects), - "box_voxels": make_box_voxels(), - "fresh_ids": make_fresh_ids(), - "seed_labels": make_seed_labels(), - "labels_for_box": make_labels_for_box(), - "paint_box": make_paint_box(), - "prompts": make_prompts(), - "organelles": make_organelles(), - } - - -MAKERS = {"ai_annotate.json": make_ai_annotate} - - -def write_fixtures(out_dir: Path) -> None: - out_dir.mkdir(parents=True, exist_ok=True) - for name, make in MAKERS.items(): - text = fixture_text(make()) - (out_dir / name).write_text(text, encoding="utf-8") - size = len(text.encode("utf-8")) - assert size < 600_000, f"{name} is {size} bytes; keep fixtures small enough to commit" - print(f"wrote {out_dir / name} ({size} bytes)") - - -def check_fixtures(committed_dir: Path) -> int: - with tempfile.TemporaryDirectory() as tmp: - fresh_dir = Path(tmp) - write_fixtures(fresh_dir) - problems: list[str] = [] - for name in FIXTURES: - committed_path = committed_dir / name - if not committed_path.exists(): - problems.append(f"{name}: missing") - continue - committed = json.loads(committed_path.read_text(encoding="utf-8")) - fresh = json.loads((fresh_dir / name).read_text(encoding="utf-8")) - differences(name, committed, fresh, problems) - if problems: - print(f"{len(problems)} difference(s) between the committed fixture and the Python code:") - for problem in problems[:40]: - print(f" {problem}") - if len(problems) > 40: - print(f" ... and {len(problems) - 40} more") - print(f"Regenerate with: python {Path(__file__).relative_to(ROOT)}") - return 1 - print("AI annotation fixture is up to date") - return 0 - - -def main(argv=None) -> int: - parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) - parser.add_argument("--out", type=Path, default=HERE, help="directory to write into (default: beside this script)") - parser.add_argument("--check", action="store_true", help="regenerate to a temporary directory and compare; exit 1 if stale") - args = parser.parse_args(argv) - if args.check: - return check_fixtures(args.out) - write_fixtures(args.out) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/web/test/fixtures/make_fixtures.py b/web/test/fixtures/make_fixtures.py index 1063f07e..4cf08f36 100644 --- a/web/test/fixtures/make_fixtures.py +++ b/web/test/fixtures/make_fixtures.py @@ -6,15 +6,14 @@ (``web/src/infer/post.ts`` from ``cellmap_flow/post/postprocessors.py``, with the segmentation steps in ``affinity.ts`` and ``morton.ts``, which also port ``cellmap_flow/post/segment.py``'s ``mutex_watershed`` and pymorton's -interleave), the layer URL's args blob (``web/src/shared/args.ts`` from -``cellmap_flow/pipeline_spec.py`` and ``serving/protocol.py``) and the -finetune's target transforms (``web/src/finetune/targets.ts`` from -``cellmap_flow/finetune/target_transforms.py``). A shared link works across -the two modes only if these agree byte for byte, and a model finetuned in the -browser learns what the server's would only if the targets do, so the vitest +interleave) and the layer URL's args blob (``web/src/shared/args.ts`` from +``cellmap_flow/pipeline_spec.py`` and ``serving/protocol.py``). A shared link +works across the two modes only if these agree byte for byte, so the vitest suite in ``web/test/parity`` checks each port against what the *Python classes themselves* produce, written here as JSON, rather than against a reading of -the Python code. +the Python code. (The finetune's target transforms had a port and a fixture +here too; the browser now runs the Python itself, ``cellmap_flow/portable/ +targets.py``, so there is nothing to keep in step.) Run it with a Python that has cellmap_flow's dependencies, from anywhere:: @@ -52,11 +51,6 @@ ROOT = HERE.parents[2] sys.path.insert(0, str(ROOT)) -from cellmap_flow.finetune.target_transforms import ( # noqa: E402 - AffinityTargetTransform, - BinaryTargetTransform, - BroadcastBinaryTargetTransform, -) from cellmap_flow.pipeline_spec import ( # noqa: E402 PipelineSpec, chain_is_segmentation, @@ -72,7 +66,7 @@ from cellmap_flow.serving.protocol import ARGS_KEY # noqa: E402 from cellmap_flow.viewer.layers import prediction_url # noqa: E402 -FIXTURES = ("norms.json", "post.json", "args.json", "affinity.json", "morton.json", "targets.json") +FIXTURES = ("norms.json", "post.json", "args.json", "affinity.json", "morton.json") # The dashboard a browser-mode blob names, and the model/host the layer URL # fixture uses. Any values do; they only have to be the same on both sides. @@ -666,114 +660,9 @@ def make_morton() -> dict: } -# --- targets.json ------------------------------------------------------------- - -# The unit offsets of the browser's affinity models (channels *_aff_z, *_aff_y, -# *_aff_x), the order their manifests name them in. -TARGET_OFFSETS = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] -TARGET_BLOCKS = [ - # (name, shape, dtype, the labels drawn from; None for all zero). Labels - # are what the paint layer writes: 0 unpainted, 1 background, 2+ objects. - # A one-slice block is a 2D student's chunk, where the z offset pairs - # nothing; the dense block has no unpainted voxel, so every in-chunk pair - # is masked; the uint16 block has an instance volume's ids past 255. - ("slice_1_6_7", (1, 6, 7), np.uint8, [0, 1, 2, 3]), - ("block_4_5_6", (4, 5, 6), np.uint8, [0, 1, 2, 3, 4]), - ("block_2_3_3", (2, 3, 3), np.uint8, [0, 1, 2]), - ("dense_3_4_4", (3, 4, 4), np.uint8, [1, 2, 3]), - ("ids_2_3_4_uint16", (2, 3, 4), np.uint16, [0, 1, 2, 300, 40000]), - ("zeros_1_4_4", (1, 4, 4), np.uint8, None), -] -TARGET_TRANSFORMS = [ - # (id prefix, class name, constructor arguments). Four channels for three - # offsets: the fourth is masked out, as num_channels says of LSD channels. - ("binary", "BinaryTargetTransform", {}), - ("broadcast_3", "BroadcastBinaryTargetTransform", {"num_channels": 3}), - ("affinity_3", "AffinityTargetTransform", {"offsets": TARGET_OFFSETS, "num_channels": 3}), - ("affinity_4", "AffinityTargetTransform", {"offsets": TARGET_OFFSETS, "num_channels": 4}), -] -TARGET_CLASSES = { - "BinaryTargetTransform": BinaryTargetTransform, - "BroadcastBinaryTargetTransform": BroadcastBinaryTargetTransform, - "AffinityTargetTransform": AffinityTargetTransform, -} - - -def target_blocks(seed: int) -> dict: - """The label blocks of TARGET_BLOCKS, by name, drawn with one generator in order.""" - rng = np.random.default_rng(seed) - blocks = {} - for name, shape, dtype, values in TARGET_BLOCKS: - if values is None: - blocks[name] = np.zeros(shape, dtype=dtype) - else: - blocks[name] = rng.choice(np.array(values, dtype=dtype), size=shape) - return blocks - - -def make_targets() -> dict: - import torch - - seed = 0 - blocks = target_blocks(seed) - # Every block has an object next to a same-labelled voxel and next to a - # differently labelled one along some offset, so the affinity cases - # have both ones and zeros in their masked targets (the all-zero block - # and the one-slice block's z channel aside, which are there for their - # empty masks). - for name, block in blocks.items(): - if name.startswith("zeros"): - continue - same = any(((block[s] == block[d]) & (block[s] > 1)).any() for s, d in _target_pairs(block.shape)) - different = any(((block[s] != block[d]) & (block[s] > 0) & (block[d] > 0)).any() for s, d in _target_pairs(block.shape)) - assert same and different, f"{name}: the seed gives no same-object pair or no different pair" - cases = [] - for prefix, class_name, params in TARGET_TRANSFORMS: - transform = TARGET_CLASSES[class_name](**params) - for name, block in blocks.items(): - # As the server's dataset hands a patch to the transform: a float32 - # tensor (exact for every id below 2**24), one sample, one channel. - annotation = torch.from_numpy(block.astype(np.float32))[None, None] - target, mask = transform(annotation) - assert target.shape[0] == 1 and mask.shape[0] == 1 and target.shape == mask.shape, f"{prefix}_{name}" - assert target.shape[2:] == block.shape and mask.shape[2:] == block.shape, f"{prefix}_{name}" - cases.append( - { - "id": f"{prefix}_{name}", - "transform": class_name, - "params": params, - "input": name, - "target": array_record(target[0].numpy()), - "mask": array_record(mask[0].numpy()), - } - ) - return { - "_format": ( - "Target transforms, from cellmap_flow/finetune/target_transforms.py. `inputs` are label blocks " - "[z, y, x] by name (0 unpainted, 1 background, 2+ objects, the paint layer's values); each case " - "builds its `transform` class with `params`, applies it to the named input as the server's " - "dataset does (a float32 tensor of shape (1, 1, z, y, x)) and records the `target` and `mask` it " - "returned with the batch axis dropped: float32 [C, z, y, x], the channel outermost, which is " - "the browser's channels × N layout. Arrays are {dtype, shape, data} with data flat in C order. " - "Generated by make_fixtures.py; do not edit." - ), - "seed": seed, - "inputs": {name: array_record(block) for name, block in blocks.items()}, - "cases": cases, - } - - -def _target_pairs(shape): - """(source, neighbour) index tuples of the voxel pairs along each TARGET_OFFSET inside a block.""" - for offset in TARGET_OFFSETS: - src = tuple(slice(max(0, -d), min(n, n - d)) for d, n in zip(offset, shape)) - dst = tuple(slice(max(0, d), min(n, n + d)) for d, n in zip(offset, shape)) - yield src, dst - - # --- writing and checking ----------------------------------------------------- -MAKERS = {"norms.json": make_norms, "post.json": make_post, "args.json": make_args, "affinity.json": make_affinity, "morton.json": make_morton, "targets.json": make_targets} +MAKERS = {"norms.json": make_norms, "post.json": make_post, "args.json": make_args, "affinity.json": make_affinity, "morton.json": make_morton} def write_fixtures(out_dir: Path) -> None: diff --git a/web/test/fixtures/make_label_it_fixtures.py b/web/test/fixtures/make_label_it_fixtures.py index 49bebcdd..079c4ddb 100644 --- a/web/test/fixtures/make_label_it_fixtures.py +++ b/web/test/fixtures/make_label_it_fixtures.py @@ -1,21 +1,20 @@ """Generate the parity fixture for the browser's Label it tools. -The browser mode re-implements the Finetune tab's Seed from Prediction, All -Background, Split Objects and Undo in TypeScript (``web/src/label-it/``: -``segment.ts`` and ``prediction.ts`` from -``cellmap_flow/dashboard/routes/finetune/view_labels.py``; ``relabelObjects`` -in ``web/src/ai-annotate/labels.ts`` and ``rewriteForeground`` in -``web/src/ai-annotate/fill.ts`` from ``cellmap_flow/finetune/session/fill.py``). -A seed made in the browser must find the objects the server's seed finds in -the same prediction, read the prediction under the same voxels, and a split -must give the same ids, so the vitest suite in -``web/test/parity/label-it.test.ts`` checks each port against what the +A seed's maths is the Python both modes run +(``cellmap_flow/portable/label_it.py``) save one step: the browser runs the +mutex watershed in TypeScript (``web/src/label-it/segment.ts`` +``mutexObjects``, on ``web/src/infer/affinity.ts``), since ``mwatershed`` +has no Pyodide wheel. (The ids, writes and undo of Seed, All Background, +Split Objects and Undo are ``cellmap_flow/portable/fill.py``'s in both modes, +tested by pytest.) A seed made in the browser must find the objects the +server's seed finds in the same affinities, so the vitest suite in +``web/test/parity/label-it.test.ts`` checks the watershed against what the *Python functions themselves* produce, written here as JSON. -A separate generator from ``make_fixtures.py`` and ``make_ai_fixtures.py`` -(whose helpers and conventions it shares). Run it with a Python that has -cellmap_flow's dependencies (numpy, scipy, zarr, mwatershed, fastremap, -flask), from anywhere:: +A separate generator from ``make_fixtures.py`` (whose helpers and +conventions it shares). Run it with a Python that has +cellmap_flow's dependencies (numpy, scipy, mwatershed, fastremap, flask), +from anywhere:: .pixi/envs/default/bin/python web/test/fixtures/make_label_it_fixtures.py # regenerate .pixi/envs/default/bin/python web/test/fixtures/make_label_it_fixtures.py --check # CI: stale? @@ -27,41 +26,17 @@ Everything is deterministic (seeds recorded in the file) and the script is idempotent. Arrays are ``{"dtype", "shape", "data"}`` with ``data`` flat in C -order (``make_fixtures.array_record``). The sections of ``label_it.json``: - -``blocks`` - the predictions the cases segment, [C, z, y, x] float32: ``aff``, three - affinity channels of logits; ``aff_unit``, three of probabilities; - ``aff_small``, three of logits, small; ``binary``, one channel of - logits; ``unit``, a probability; ``signed``, two channels in [-1, 1] (a - tanh). Smoothed noise, so their objects are blobs, as a model's are. -``probability`` - ``view_labels.probability`` of blocks for an output class (or none, read - off the values) and channels: float32, compared within the float - tolerance (scipy's float32 expit and the port's agree to an ulp). -``segment`` - ``view_labels.segment_prediction``'s objects for components and the - mutex watershed over thresholds, smallest sizes, connectivities and per - slice. The blocks' seed is the first that keeps every voxel's - probability clear of its threshold, every mutex-watershed edge clear of - ties (make_fixtures' affinity_case_is_safe) and, through expit, its - edges 1e-6 apart, so a float32 ulp cannot move a voxel. -``relabel_objects`` - ``fill.relabel_objects`` of label boxes (walls in every slice or one, - strokes joining objects, ties, uint16 ids, ids running out with the - refusal's message). -``rewrite_foreground`` - ``fill.rewrite_foreground`` with ``relabel_objects`` on an 8^3 volume in - 4^3 chunks, MinIO replaced by an in-memory zarr as make_ai_fixtures' - paint_box cases replace it: the volume before, the changed count and - counts, the volume after, the undo entry, and ``fill.restore_box`` after - a stroke painted in between. -``read_prediction`` - ``view_labels.read_prediction`` of a served array (the model's output on - its own grid, OME translation and voxel size, in chunks) for an - annotation volume's box, its server's HTTP replaced by the array's - files: the prediction and the box shrunk to what the server covers, or - the refusal's message when it covers nothing. +order (``make_fixtures.array_record``). The section of ``label_it.json``: + +``mutex_watershed`` + ``view_labels.segment_prediction``'s objects by the mutex watershed over + thresholds (its bias) and smallest sizes, with the ``affinities`` the + browser's Python hands its watershed (``label_it.affinities`` of a + prediction: three affinity channels of probabilities, or of logits, + small, through the sigmoid; smoothed noise, so their objects are blobs, + as a model's are), [C, z, y, x] float32. The predictions' seed is the + first that keeps every edge clear of ties (make_fixtures' + affinity_case_is_safe), which mwatershed would break its own way. """ from __future__ import annotations @@ -71,25 +46,20 @@ import sys import tempfile from pathlib import Path -from types import SimpleNamespace import numpy as np -import zarr HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) -from make_fixtures import ROOT, affinity_case_is_safe, affinity_edge_weights, array_record, differences, fixture_text # noqa: E402 (puts the worktree's package first on sys.path) +from make_fixtures import ROOT, affinity_case_is_safe, array_record, differences, fixture_text # noqa: E402 (puts the worktree's package first on sys.path) from cellmap_flow.dashboard.routes.finetune import view_labels # noqa: E402 -from cellmap_flow.finetune.session import fill # noqa: E402 +from cellmap_flow.portable import label_it # noqa: E402 FIXTURES = ("label_it.json",) SEED = 20261016 OFFSETS = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] -# How far a voxel's mean probability must sit from its threshold, so the -# port's float32 expit (an ulp from scipy's on a few values) decides it the same. -PROBABILITY_MARGIN = 1e-5 # --- the predictions ------------------------------------------------------------ @@ -116,42 +86,16 @@ def _sigmoid(x: np.ndarray) -> np.ndarray: def make_blocks(seed: int) -> dict: rng = np.random.default_rng(seed) return { - "aff": _affinities(rng, (6, 10, 12)).astype(np.float32), - # Probabilities already (output class unit), for the mutex watershed's - # larger cases: its input is then the fixture's own float32 values, with - # no expit between, and only its own ties matter (affinity_case_is_safe). - # A few hundred voxels, as make_fixtures' affinity blocks: near 1, - # float32's spacing makes exact ties likely among thousands of edges. + # Probabilities already (output class unit): the watershed's input is + # then the fixture's own float32 values. A few hundred voxels, as + # make_fixtures' affinity blocks: near 1, float32's spacing makes + # exact ties likely among thousands of edges. "aff_unit": _sigmoid(_affinities(rng, (4, 8, 9))).astype(np.float32), - # Logits for the mutex watershed through expit: small and of a modest - # range, so its edges can be kept a whole 1e-6 apart, which an ulp of - # expit cannot reorder. + # Logits, through the Python's sigmoid: small and of a modest range. "aff_small": _affinities(rng, (3, 5, 6), amplitude=1.2).astype(np.float32), - "binary": (3.0 * _field(rng, (6, 9, 10)))[None].astype(np.float32), - "unit": _sigmoid(2.5 * _field(rng, (5, 6, 7)))[None].astype(np.float32), - "signed": np.tanh(np.stack([_field(rng, (4, 6, 6)) for _ in range(2)])).astype(np.float32), } -def _channels(channels): - """The fixture's channel list as the Python's slice (contiguous here), or None.""" - return None if channels is None else slice(channels[0], channels[-1] + 1) - - -# id, block, output class, channels, threshold, min_size, connectivity, per_slice -COMPONENT_CASES = [ - ("aff_default", "aff", "unbounded", [0, 1, 2], 0.5, 0, 1, False), - ("aff_low_edges", "aff", "unbounded", [0, 1, 2], 0.3, 0, 2, False), - ("aff_classified_all_min3", "aff", None, [0, 1, 2], 0.7, 3, 3, False), - ("aff_one_channel_per_slice", "aff", "unbounded", [1], 0.5, 0, 1, True), - ("binary_default", "binary", "unbounded", None, 0.5, 0, 1, False), - ("binary_per_slice_min5", "binary", "unbounded", None, 0.5, 5, 1, True), - ("binary_per_slice_all", "binary", "unbounded", None, 0.6, 0, 3, True), - ("unit_default", "unit", "unit", None, 0.5, 0, 1, False), - ("unit_classified_edges", "unit", None, None, 0.4, 0, 2, True), - ("signed_default", "signed", "signed_unit", None, 0.5, 0, 1, False), - ("signed_classified_min2", "signed", None, None, 0.6, 2, 1, False), -] # id, block, output class, threshold (the bias), min_size MUTEX_CASES = [ ("mutex_default", "aff_unit", "unit", 0.5, 0), @@ -161,37 +105,14 @@ def _channels(channels): ("mutex_logits", "aff_small", "unbounded", 0.5, 0), ("mutex_logits_classified_min2", "aff_small", None, 0.4, 2), ] -# How far apart the edges of a mutex case through expit must be (and from -# 0): an ulp of expit, ~6e-8 at most, then cannot reorder them. -EXPIT_EDGE_MARGIN = 1e-6 -# id, block, output class, channels -PROBABILITY_CASES = [ - ("aff_unbounded", "aff_small", "unbounded", [0, 1, 2]), - ("aff_one_channel", "aff_small", "unbounded", [2]), - ("aff_classified", "aff_small", None, None), - ("binary_unbounded", "binary", "unbounded", None), - ("unit_unit", "unit", "unit", None), - ("unit_classified", "unit", None, None), - ("signed_signed", "signed", "signed_unit", None), - ("signed_classified", "signed", None, [1]), -] def _safe(blocks: dict) -> bool: - """No component case's voxel near its threshold, no mutex case near a tie.""" - for _, name, cls, channels, threshold, *_ in COMPONENT_CASES: - mean = view_labels.probability(blocks[name], cls, _channels(channels)).mean(axis=0) - if np.min(np.abs(mean.astype(np.float64) - np.float32(threshold))) <= PROBABILITY_MARGIN: - return False - for _, name, cls, threshold, _ in MUTEX_CASES: - affinities = view_labels.probability(blocks[name], cls, slice(0, len(OFFSETS))) - if not affinity_case_is_safe(affinities, OFFSETS, threshold): - return False - if name == "aff_small": # through expit - weights = np.sort(affinity_edge_weights(affinities, OFFSETS, threshold)) - if weights[0] <= EXPIT_EDGE_MARGIN or np.min(np.diff(weights)) <= EXPIT_EDGE_MARGIN: - return False - return True + """No mutex case near a tie.""" + return all( + affinity_case_is_safe(label_it.affinities(blocks[name], cls, OFFSETS), OFFSETS, threshold) + for _, name, cls, threshold, _ in MUTEX_CASES + ) def make_predictions() -> tuple[int, dict]: @@ -199,224 +120,23 @@ def make_predictions() -> tuple[int, dict]: blocks = make_blocks(seed) if _safe(blocks): return seed, blocks - raise RuntimeError("no seed in 200 keeps the label-it fixture clear of thresholds and ties") - - -def make_probability(blocks: dict) -> list: - cases = [] - for case_id, name, cls, channels in PROBABILITY_CASES: - out = view_labels.probability(blocks[name], cls, _channels(channels)) - cases.append({"id": case_id, "block": name, "output_class": cls, "channels": channels, "probability": array_record(out)}) - return cases + raise RuntimeError("no seed in 200 keeps the label-it fixture clear of ties") -def make_segment(blocks: dict) -> list: +def make_mutex(blocks: dict) -> list: cases = [] - for case_id, name, cls, channels, threshold, min_size, connectivity, per_slice in COMPONENT_CASES: - objects = view_labels.segment_prediction( - "components", blocks[name], cls, _channels(channels), None, threshold, min_size, connectivity, per_slice - ) - cases.append({ - "id": case_id, "method": "components", "block": name, "output_class": cls, "channels": channels, "offsets": None, - "threshold": threshold, "min_size": min_size, "connectivity": connectivity, "per_slice": per_slice, - "objects": array_record(objects), "count": int(objects.max()), - }) for case_id, name, cls, threshold, min_size in MUTEX_CASES: objects = view_labels.segment_prediction( - "mutex_watershed", blocks[name], cls, _channels([0, 1, 2]), json.dumps(OFFSETS), threshold, min_size + "mutex_watershed", blocks[name], cls, slice(0, len(OFFSETS)), json.dumps(OFFSETS), threshold, min_size ) cases.append({ - "id": case_id, "method": "mutex_watershed", "block": name, "output_class": cls, "channels": [0, 1, 2], "offsets": OFFSETS, - "threshold": threshold, "min_size": min_size, "connectivity": 1, "per_slice": False, + "id": case_id, "output_class": cls, "offsets": OFFSETS, "threshold": threshold, "min_size": min_size, + "affinities": array_record(label_it.affinities(blocks[name], cls, OFFSETS)), "objects": array_record(objects), "count": int(objects.max()), }) return cases -# --- relabel_objects ------------------------------------------------------------ - - -def _box(shape, dtype, *blocks): - """A label box of `shape` with each (value, index) of `blocks` painted in turn.""" - box = np.zeros(shape, dtype=dtype) - for value, index in blocks: - box[index] = value - return box - - -def _ids_run_out() -> np.ndarray: - """A uint8 box using every id 2..255 on its own voxel, and id 5 on one - more voxel apart from the first: the split needs a fresh id, none is left.""" - box = np.zeros((1, 24, 48), dtype=np.uint8) - spots = [(0, 2 * r, 2 * c) for r in range(12) for c in range(24)] - for value, spot in zip(range(2, 256), spots): - box[spot] = value - box[0, 23, 1] = 5 - return box - - -RELABEL_CASES = [ - # A background wall through every slice: the two sides apart, one keeps 5. - ("wall_cuts", _box((3, 5, 8), np.uint8, (5, np.s_[:, 1:4, 1:7]), (1, np.s_[:, :, 4])), 1, False), - # The wall in one slice only: the sides still touch above and below it. - ("wall_one_slice", _box((3, 5, 8), np.uint8, (5, np.s_[:, 1:4, 1:7]), (1, np.s_[1, :, 4])), 1, False), - # ... and per slice, each slice's pieces apart: the middle slice splits, every slice its own id. - ("wall_one_slice_per_slice", _box((3, 5, 8), np.uint8, (5, np.s_[:, 1:4, 1:7]), (1, np.s_[1, :, 4])), 1, True), - # A stroke of 3 joining objects 3 and 7: one component, the id most of it holds. - ("stroke_merges", _box((2, 4, 9), np.uint8, (3, np.s_[:, 1:3, 0:3]), (7, np.s_[:, 1:3, 5:9]), (3, np.s_[:, 1:3, 3:5])), 1, False), - # As many voxels of 4 as of 6 in one component: the larger id stays. - ("tie_takes_larger_id", _box((1, 2, 4), np.uint8, (4, np.s_[0, 0, :]), (6, np.s_[0, 1, :])), 1, False), - # Id 5 on three components of 3, 8 and 8 voxels: the first 8 keeps it, the others get the lowest free ids. - ("keep_largest_first", _box((1, 7, 9), np.uint8, (5, np.s_[0, 0, 0:3]), (5, np.s_[0, 2, 0:8]), (5, np.s_[0, 4, 1:9]), (2, np.s_[0, 6, 0]), (4, np.s_[0, 6, 8])), 1, False), - # Two parts touching only along an edge or at a corner: apart with faces, one with all neighbours. - ("diagonal_faces", _box((2, 4, 4), np.uint8, (6, np.s_[0, 0:2, 0:2]), (6, np.s_[1, 2:4, 2:4]), (9, np.s_[0, 3, 3])), 1, False), - ("diagonal_edges", _box((2, 4, 4), np.uint8, (6, np.s_[0, 0:2, 0:2]), (6, np.s_[1, 2:4, 2:4]), (9, np.s_[0, 3, 3])), 2, False), - ("diagonal_all", _box((2, 4, 4), np.uint8, (6, np.s_[0, 0:2, 0:2]), (6, np.s_[1, 2:4, 2:4]), (9, np.s_[0, 3, 3])), 3, False), - # uint16 ids, unpainted and background voxels left alone. - ("uint16", _box((2, 3, 10), np.uint16, (1, np.s_[:, :, :]), (0, np.s_[:, 2, :]), (300, np.s_[:, 0, 0:4]), (300, np.s_[:, 0, 6:10]), (301, np.s_[0, 1, 2])), 1, False), - ("no_foreground", _box((2, 2, 2), np.uint8, (1, np.s_[0])), 1, False), - ("ids_run_out", _ids_run_out(), 1, False), -] - - -def make_relabel() -> list: - cases = [] - for case_id, existing, connectivity, per_slice in RELABEL_CASES: - case = {"id": case_id, "existing": array_record(existing), "connectivity": connectivity, "per_slice": per_slice} - try: - labels, counts = fill.relabel_objects(existing, connectivity=connectivity, per_slice=per_slice) - except ValueError as e: - case["error"] = str(e) - else: - case["labels"] = array_record(labels) - case["counts"] = {k: int(v) for k, v in counts.items()} - cases.append(case) - return cases - - -# --- rewrite_foreground and restore_box ----------------------------------------- - - -def make_rewrite() -> list: - from make_ai_fixtures import STATE, _served - - cut = np.zeros((8, 8, 8), dtype=np.uint8) - cut[1:7, 1:7, 1:7] = 5 - cut[1:7, 1:7, 4] = 1 # a wall through every slice of the object, across the chunks - cut[0, 0, 0] = 9 # outside the box: never touched - joined = cut.copy() - joined[2:5, 2:5, 4] = 5 # a stroke through the wall: one object again - uint16 = cut.astype(np.uint16) - uint16[cut == 5] = 400 - cases = [] - # id, initial, lo, hi, connectivity, per_slice, stroke painted before the restore - for case_id, initial, lo, hi, connectivity, per_slice, stroke in [ - ("wall_across_chunks", cut, (1, 1, 1), (7, 7, 7), 1, False, [((3, 3, 2), 1), ((3, 3, 6), 7)]), - ("joined_nothing_changes", joined, (1, 1, 1), (7, 7, 7), 1, False, None), - ("per_slice", cut, (2, 0, 0), (5, 8, 8), 1, True, [((4, 2, 2), 3)]), - ("uint16", uint16, (0, 0, 0), (8, 8, 8), 1, False, [((2, 2, 5), 1)]), - ]: - served = _served(initial) - undo = [] - relabel = lambda existing, c=connectivity, p=per_slice: fill.relabel_objects(existing, connectivity=c, per_slice=p) # noqa: E731 - changed, counts = fill.rewrite_foreground(STATE, "vol-1", np.asarray(lo), np.asarray(hi), relabel, undo=undo) - case = { - "id": case_id, "dtype": str(initial.dtype), "shape": list(initial.shape), "chunks": [4, 4, 4], "initial": array_record(initial), - "lo": list(lo), "hi": list(hi), "connectivity": connectivity, "per_slice": per_slice, - "changed": int(changed), "counts": {k: int(v) for k, v in counts.items()}, "after": array_record(served[:]), - "undo": None if not undo else {"before": array_record(undo[0][2]), "after": array_record(undo[0][3])}, - } - if stroke is not None and undo: - for index, value in stroke: - served[tuple(index)] = value - restored = fill.restore_box(STATE, "vol-1", np.asarray(lo), np.asarray(hi), undo[0][2], undo[0][3]) - case["restore"] = {"stroke": [[list(index), int(value)] for index, value in stroke], "restored": int(restored), "final": array_record(served[:])} - cases.append(case) - return cases - - -# --- read_prediction ------------------------------------------------------------- - - -class _Reply: - """A model server's answer, as requests gives it: .json() and .content.""" - - def __init__(self, content: bytes): - self.content = content - - def json(self): - return json.loads(self.content) - - -def _serve(values: np.ndarray, voxel, translation, chunks): - """The files of a served array, [C, z, y, x], at OME `voxel` and - `translation` (voxel 0's centre), keyed by URL as read_prediction asks - for them, and its read in place of the HTTP one (view_labels._get).""" - store = {} - zarr.create(shape=values.shape, chunks=(values.shape[0], *chunks), dtype=values.dtype, compressor=None, store=store, fill_value=0)[:] = values - files = {f"base/s0/{key}": value for key, value in store.items()} - files["base/.zattrs"] = json.dumps({"multiscales": [{ - "axes": [{"name": "c", "type": "channel"}] + [{"name": a, "type": "space", "unit": "nanometer"} for a in "zyx"], - "datasets": [{"path": "s0", "coordinateTransformations": [ - {"type": "scale", "scale": [1.0, *map(float, voxel)]}, {"type": "translation", "translation": [0.0, *map(float, translation)]}, - ]}], - }]}).encode() - return lambda url, timeout=None: _Reply(files[url]) - - -def _served_values(shape) -> np.ndarray: - """A served block whose value names its voxel: c·10^6 + z·10^4 + y·100 + x (exact in float32 here).""" - c, z, y, x = np.meshgrid(*(np.arange(n) for n in shape), indexing="ij") - return (c * 1_000_000 + z * 10_000 + y * 100 + x).astype(np.float32) - - -# id, served (shape [C, z, y, x], voxel, translation, chunks), volume (voxel, dataset_offset_nm), lo, hi -READ_CASES = [ - # The grids are one: the box as it is, across the served chunks. - ("same_grid", ((2, 12, 12, 12), (8, 8, 8), (4, 4, 4), (4, 4, 4)), ((8, 8, 8), (4, 4, 4)), (2, 3, 1), (9, 10, 11)), - # The model served at twice the voxel: two annotation voxels to a served one. - ("coarser_served", ((1, 8, 8, 8), (16, 16, 16), (8, 8, 8), (4, 4, 4)), ((8, 8, 8), (4, 4, 4)), (1, 2, 3), (9, 12, 15)), - # At half: every other served voxel, the one each annotation voxel's centre lies in. - ("finer_served", ((1, 20, 20, 20), (4, 4, 4), (2, 2, 2), (5, 5, 5)), ((8, 8, 8), (4, 4, 4)), (0, 1, 2), (6, 7, 9)), - # Served from 20 nm on: annotation voxels before it are left out, the box shrinks. - ("offset_served_clips_low", ((1, 10, 10, 10), (8, 8, 8), (24, 24, 24), (4, 4, 4)), ((8, 8, 8), (4, 4, 4)), (0, 1, 2), (6, 7, 8)), - # Half a voxel off: each centre lies in one served voxel all the same. - ("half_voxel_shift", ((1, 10, 10, 10), (8, 8, 8), (8, 8, 8), (5, 5, 5)), ((8, 8, 8), (4, 4, 4)), (1, 1, 1), (8, 8, 8)), - # The box runs past the served array's end: shrunk to it. - ("clips_high", ((1, 6, 6, 6), (8, 8, 8), (4, 4, 4), (4, 4, 4)), ((8, 8, 8), (4, 4, 4)), (3, 2, 4), (9, 5, 12)), - # Anisotropic grids, z coarser. - ("anisotropic", ((3, 6, 12, 12), (16, 8, 8), (8, 4, 4), (3, 4, 4)), ((16, 8, 8), (8, 4, 4)), (1, 2, 3), (5, 11, 9)), - # Anisotropic, served coarser in y and x only. - ("anisotropic_coarser_xy", ((1, 6, 6, 6), (16, 16, 16), (8, 8, 8), (3, 3, 3)), ((16, 8, 8), (8, 4, 4)), (0, 1, 1), (5, 9, 10)), - # Nothing of the box served: the refusal. - ("outside", ((1, 4, 4, 4), (8, 8, 8), (4, 4, 4), (4, 4, 4)), ((8, 8, 8), (4, 4, 4)), (5, 0, 0), (8, 4, 4)), -] - - -def make_read_prediction() -> list: - view_labels._served_base = lambda host, model_name, postprocess=(), input_norm=None: "base" - view_labels.prediction_voxel_override = lambda *args: None - view_labels.get_session = lambda: SimpleNamespace(dataset_path="data") - cases = [] - for case_id, (shape, voxel, translation, chunks), (volume_voxel, offset), lo, hi in READ_CASES: - values = _served_values(shape) - view_labels._get = _serve(values, voxel, translation, chunks) - volume = {"output_voxel_size": [float(v) for v in volume_voxel], "dataset_offset_nm": [float(v) for v in offset]} - # The served values are _served_values' formula, which the test computes again. - case = { - "id": case_id, "served": {"shape": list(shape), "voxel": list(voxel), "translation": list(translation), "chunks": list(chunks)}, - "volume": volume, "lo": list(lo), "hi": list(hi), - } - try: - prediction, new_lo, new_hi = view_labels.read_prediction("host", "model", volume, np.asarray(lo), np.asarray(hi), {}) - except view_labels._Refused as e: - case["error"] = {"message": str(e), "status": e.status} - else: - case.update({"prediction": array_record(prediction), "new_lo": [int(v) for v in new_lo], "new_hi": [int(v) for v in new_hi]}) - cases.append(case) - return cases - - # --- writing and checking --------------------------------------------------------- @@ -424,17 +144,11 @@ def make_label_it() -> dict: seed, blocks = make_predictions() return { "_format": ( - "Parity fixture for web/src/label-it (segment.ts, prediction.ts) and the Label it parts of web/src/ai-annotate " - "(labels.ts relabelObjects, fill.ts rewriteForeground) against dashboard/routes/finetune/view_labels.py and " - "finetune/session/fill.py; arrays are {dtype, shape, data} flat in C order; see make_label_it_fixtures.py's docstring" + "Parity fixture for web/src/label-it/segment.ts (mutexObjects) against dashboard/routes/finetune/view_labels.py " + "(segment_prediction); arrays are {dtype, shape, data} flat in C order; see make_label_it_fixtures.py's docstring" ), "seed": seed, - "blocks": {name: array_record(block) for name, block in blocks.items()}, - "probability": make_probability(blocks), - "segment": make_segment(blocks), - "relabel_objects": make_relabel(), - "rewrite_foreground": make_rewrite(), - "read_prediction": make_read_prediction(), + "mutex_watershed": make_mutex(blocks), } diff --git a/web/test/fixtures/targets.json b/web/test/fixtures/targets.json deleted file mode 100644 index 844ca786..00000000 --- a/web/test/fixtures/targets.json +++ /dev/null @@ -1,470 +0,0 @@ -{ - "_format": "Target transforms, from cellmap_flow/finetune/target_transforms.py. `inputs` are label blocks [z, y, x] by name (0 unpainted, 1 background, 2+ objects, the paint layer's values); each case builds its `transform` class with `params`, applies it to the named input as the server's dataset does (a float32 tensor of shape (1, 1, z, y, x)) and records the `target` and `mask` it returned with the batch axis dropped: float32 [C, z, y, x], the channel outermost, which is the browser's channels × N layout. Arrays are {dtype, shape, data} with data flat in C order. Generated by make_fixtures.py; do not edit.", - "seed": 0, - "inputs": { - "slice_1_6_7": { - "dtype": "uint8", - "shape": [1, 6, 7], - "data": [3, 2, 2, 1, 1, 0, 0, 0, 0, 3, 2, 3, 2, 2, 3, 2, 2, 2, 2, 3, 1, 3, 2, 0, 1, 3, 2, 0, 3, 2, 3, 0, 0, 3, 0, 2, 0, 1, 1, 1, 1, 0] - }, - "block_4_5_6": { - "dtype": "uint8", - "shape": [4, 5, 6], - "data": [0, 0, 0, 3, 2, 3, 1, 3, 3, 1, 2, 4, 4, 4, 1, 3, 4, 3, 4, 3, 3, 1, 4, 0, 2, 3, 4, 2, 1, 1, 2, 2, 3, 4, 0, 4, 2, 1, 3, 2, 1, 1, 3, 2, 2, 1, 3, 1, 1, 4, 1, 1, 3, 3, 0, 0, 1, 4, 2, 3, 1, 1, 3, 4, 0, 0, 3, 1, 2, 0, 4, 2, 4, 3, 3, 1, 3, 0, 2, 2, 4, 0, 4, 0, 3, 2, 4, 1, 4, 3, 4, 0, 3, 4, 0, 1, 3, 0, 2, 3, 3, 4, 2, 2, 2, 4, 0, 2, 0, 2, 4, 3, 1, 4, 3, 4, 0, 2, 4, 3] - }, - "block_2_3_3": { - "dtype": "uint8", - "shape": [2, 3, 3], - "data": [1, 1, 1, 1, 0, 2, 0, 1, 0, 2, 2, 2, 2, 2, 0, 0, 0, 2] - }, - "dense_3_4_4": { - "dtype": "uint8", - "shape": [3, 4, 4], - "data": [3, 3, 3, 3, 3, 1, 1, 3, 1, 3, 3, 3, 2, 1, 2, 3, 2, 3, 2, 3, 1, 2, 1, 1, 3, 3, 1, 3, 3, 1, 2, 2, 2, 2, 1, 3, 3, 1, 3, 3, 1, 2, 2, 1, 3, 3, 1, 1] - }, - "ids_2_3_4_uint16": { - "dtype": "uint16", - "shape": [2, 3, 4], - "data": [0, 300, 0, 2, 40000, 40000, 1, 0, 2, 40000, 300, 0, 300, 1, 1, 2, 40000, 40000, 0, 2, 300, 1, 1, 1] - }, - "zeros_1_4_4": { - "dtype": "uint8", - "shape": [1, 4, 4], - "data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - } - }, - "cases": [ - { - "id": "binary_slice_1_6_7", - "transform": "BinaryTargetTransform", - "params": {}, - "input": "slice_1_6_7", - "target": { - "dtype": "float32", - "shape": [1, 1, 6, 7], - "data": [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [1, 1, 6, 7], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0] - } - }, - { - "id": "binary_block_4_5_6", - "transform": "BinaryTargetTransform", - "params": {}, - "input": "block_4_5_6", - "target": { - "dtype": "float32", - "shape": [1, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0] - }, - "mask": { - "dtype": "float32", - "shape": [1, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0] - } - }, - { - "id": "binary_block_2_3_3", - "transform": "BinaryTargetTransform", - "params": {}, - "input": "block_2_3_3", - "target": { - "dtype": "float32", - "shape": [1, 2, 3, 3], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0] - }, - "mask": { - "dtype": "float32", - "shape": [1, 2, 3, 3], - "data": [1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0] - } - }, - { - "id": "binary_dense_3_4_4", - "transform": "BinaryTargetTransform", - "params": {}, - "input": "dense_3_4_4", - "target": { - "dtype": "float32", - "shape": [1, 3, 4, 4], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [1, 3, 4, 4], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] - } - }, - { - "id": "binary_ids_2_3_4_uint16", - "transform": "BinaryTargetTransform", - "params": {}, - "input": "ids_2_3_4_uint16", - "target": { - "dtype": "float32", - "shape": [1, 2, 3, 4], - "data": [0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [1, 2, 3, 4], - "data": [0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0] - } - }, - { - "id": "binary_zeros_1_4_4", - "transform": "BinaryTargetTransform", - "params": {}, - "input": "zeros_1_4_4", - "target": { - "dtype": "float32", - "shape": [1, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [1, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "broadcast_3_slice_1_6_7", - "transform": "BroadcastBinaryTargetTransform", - "params": { - "num_channels": 3 - }, - "input": "slice_1_6_7", - "target": { - "dtype": "float32", - "shape": [3, 1, 6, 7], - "data": [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 1, 6, 7], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0] - } - }, - { - "id": "broadcast_3_block_4_5_6", - "transform": "BroadcastBinaryTargetTransform", - "params": { - "num_channels": 3 - }, - "input": "block_4_5_6", - "target": { - "dtype": "float32", - "shape": [3, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0] - } - }, - { - "id": "broadcast_3_block_2_3_3", - "transform": "BroadcastBinaryTargetTransform", - "params": { - "num_channels": 3 - }, - "input": "block_2_3_3", - "target": { - "dtype": "float32", - "shape": [3, 2, 3, 3], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 2, 3, 3], - "data": [1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0] - } - }, - { - "id": "broadcast_3_dense_3_4_4", - "transform": "BroadcastBinaryTargetTransform", - "params": { - "num_channels": 3 - }, - "input": "dense_3_4_4", - "target": { - "dtype": "float32", - "shape": [3, 3, 4, 4], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 3, 4, 4], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] - } - }, - { - "id": "broadcast_3_ids_2_3_4_uint16", - "transform": "BroadcastBinaryTargetTransform", - "params": { - "num_channels": 3 - }, - "input": "ids_2_3_4_uint16", - "target": { - "dtype": "float32", - "shape": [3, 2, 3, 4], - "data": [0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 2, 3, 4], - "data": [0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0] - } - }, - { - "id": "broadcast_3_zeros_1_4_4", - "transform": "BroadcastBinaryTargetTransform", - "params": { - "num_channels": 3 - }, - "input": "zeros_1_4_4", - "target": { - "dtype": "float32", - "shape": [3, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_3_slice_1_6_7", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 3 - }, - "input": "slice_1_6_7", - "target": { - "dtype": "float32", - "shape": [3, 1, 6, 7], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 1, 6, 7], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0] - } - }, - { - "id": "affinity_3_block_4_5_6", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 3 - }, - "input": "block_4_5_6", - "target": { - "dtype": "float32", - "shape": [3, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0] - } - }, - { - "id": "affinity_3_block_2_3_3", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 3 - }, - "input": "block_2_3_3", - "target": { - "dtype": "float32", - "shape": [3, 2, 3, 3], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 2, 3, 3], - "data": [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_3_dense_3_4_4", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 3 - }, - "input": "dense_3_4_4", - "target": { - "dtype": "float32", - "shape": [3, 3, 4, 4], - "data": [0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 3, 4, 4], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0] - } - }, - { - "id": "affinity_3_ids_2_3_4_uint16", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 3 - }, - "input": "ids_2_3_4_uint16", - "target": { - "dtype": "float32", - "shape": [3, 2, 3, 4], - "data": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 2, 3, 4], - "data": [0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0] - } - }, - { - "id": "affinity_3_zeros_1_4_4", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 3 - }, - "input": "zeros_1_4_4", - "target": { - "dtype": "float32", - "shape": [3, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [3, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_4_slice_1_6_7", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 4 - }, - "input": "slice_1_6_7", - "target": { - "dtype": "float32", - "shape": [4, 1, 6, 7], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [4, 1, 6, 7], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_4_block_4_5_6", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 4 - }, - "input": "block_4_5_6", - "target": { - "dtype": "float32", - "shape": [4, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [4, 4, 5, 6], - "data": [0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_4_block_2_3_3", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 4 - }, - "input": "block_2_3_3", - "target": { - "dtype": "float32", - "shape": [4, 2, 3, 3], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [4, 2, 3, 3], - "data": [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_4_dense_3_4_4", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 4 - }, - "input": "dense_3_4_4", - "target": { - "dtype": "float32", - "shape": [4, 3, 4, 4], - "data": [0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [4, 3, 4, 4], - "data": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_4_ids_2_3_4_uint16", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 4 - }, - "input": "ids_2_3_4_uint16", - "target": { - "dtype": "float32", - "shape": [4, 2, 3, 4], - "data": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [4, 2, 3, 4], - "data": [0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - }, - { - "id": "affinity_4_zeros_1_4_4", - "transform": "AffinityTargetTransform", - "params": { - "offsets": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], - "num_channels": 4 - }, - "input": "zeros_1_4_4", - "target": { - "dtype": "float32", - "shape": [4, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - }, - "mask": { - "dtype": "float32", - "shape": [4, 1, 4, 4], - "data": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - } - } - ] -} diff --git a/web/test/parity/ai-annotate.test.ts b/web/test/parity/ai-annotate.test.ts deleted file mode 100644 index 30f1bcea..00000000 --- a/web/test/parity/ai-annotate.test.ts +++ /dev/null @@ -1,362 +0,0 @@ -// Parity of web/src/ai-annotate (geometry.ts, plane.ts, mask.ts, labels.ts, -// fill.ts, prompts.ts, organelles.ts) with cellmap_flow/ai_annotate -// (geometry.py, pipeline.py, mask_extraction.py, prompts.py, organelles.py) -// and cellmap_flow/finetune/session/fill.py. -// -// The fixture (test/fixtures/ai_annotate.json) was written by the Python -// functions themselves (test/fixtures/make_ai_fixtures.py, whose docstring -// lists the sections): plans for volumes, points, depth axes, crop sizes and -// data boxes, with their refusals' messages; raw boxes; masks found in an RGB -// image; planes normalized and resized; masks put on write boxes; ids for -// boxes with and without the planes beside; paint_box and restore_box on an -// 8^3 volume; the prompts word for word; the catalog. Integers and strings -// match exactly, floats within the parity tolerance. Regenerate the fixture -// when the Python changes; tests/web/test_fixtures_fresh.py fails when it is -// stale. -import { describe, expect, it } from "vitest"; - -import type { PaintVolume } from "../../src/contracts"; -import { boxVoxels, chunkBytes, paintBox, readBox, restoreBox, volumeCornerNm, writeBox } from "../../src/ai-annotate/fill"; -import { clickPx, fromScreen, planeAxes, planeName, planPlane, rawBox, screenShape, toScreen, writeShape } from "../../src/ai-annotate/geometry"; -import { freshIds, seedLabels } from "../../src/ai-annotate/labels"; -import { extractMask, normalizeToUint8 } from "../../src/ai-annotate/mask"; -import { findOrganelleProfile, ORGANELLES, resolveOrganelleProfile } from "../../src/ai-annotate/organelles"; -import { labelsForBox, maskToWriteShape, resizePlane } from "../../src/ai-annotate/plane"; -import { buildRecolorPrompt, editablePrompt } from "../../src/ai-annotate/prompts"; -import { AIAnnotateError, type DepthAxis, type Labels, MAX_IMAGE_SIDE, MAX_PROMPT_CHARS, type OrganelleProfile, PLANE_NAMES, type PlanePlan, type UndoEntry } from "../../src/ai-annotate/types"; -import { CHUNK_DIR, chunkKey } from "../../src/finetune/volume"; -import { MemoryPaintStore } from "../../src/state/paint-store"; -import { type ArrayRecord, expectArrayMatches, loadFixture } from "./helpers"; - -interface Refusal { category: string; message: string; http_status: number } -interface VolumeRecord { output_voxel_size: number[]; input_voxel_size: number[]; dataset_offset_nm: number[] } -interface PlanCase { - id: string; volume: VolumeRecord; volume_shape: number[]; point_nm: number[]; depth_axis: DepthAxis; crop_size_px: number; - data_box_nm: [number[], number[]] | null; display_dimensions: string[] | null; - plan?: PlanePlan; click_px?: number[]; plane_axes?: number[]; write_shape?: number[]; plane_name?: string; error?: Refusal; -} -interface PaintCase { - id: string; dtype: "uint8" | "uint16"; shape: number[]; chunks: number[]; initial: ArrayRecord; lo: number[]; hi: number[]; labels: ArrayRecord; - overwrite: boolean; counts: { n_foreground: number; n_background: number; n_overwritten: number }; after: ArrayRecord; - undo: { before: ArrayRecord; after: ArrayRecord } | null; - restore?: { stroke: [number[], number][]; restored: number; final: ArrayRecord }; -} -interface Fixture { - seed: number; - constants: { max_image_side: number; plane_names: string[] }; - screen: { depth_axis: DepthAxis; display_dimensions: string[] | null; plane: ArrayRecord; to_screen: ArrayRecord; screen_shape: number[]; image: ArrayRecord; from_screen: ArrayRecord }[]; - plan_plane: PlanCase[]; - raw_box: { plan: string; voxel_size: number[]; corner: number[]; begin: number[]; count: number[] }[]; - extract_mask: { image: ArrayRecord; cases: { color_name: string; target_rgb: [number, number, number]; threshold: number; mask: ArrayRecord }[] }; - normalize_to_uint8: { id: string; plane: ArrayRecord; uint8: ArrayRecord }[]; - resize: { id: string; plane: ArrayRecord; shape: number[]; resized: ArrayRecord }[]; - mask_to_write_shape: { id: string; plan: string; mask: ArrayRecord; write: ArrayRecord }[]; - box_voxels: { id: string; volume: VolumeRecord; offset_nm: number[]; shape_nm: number[]; volume_shape: number[]; corner_nm: number[]; box: { lo: number[]; hi: number[] } | null }[]; - fresh_ids: { id: string; existing: ArrayRecord; count: number; count_up: boolean; ids?: number[]; error?: string }[]; - seed_labels: { id: string; objects: ArrayRecord; existing: ArrayRecord; count_up: boolean; labels?: ArrayRecord; error?: string }[]; - labels_for_box: { id: string; objects: ArrayRecord; existing: ArrayRecord; depth_axis: DepthAxis; count_up: boolean; beside: (ArrayRecord | null)[] | null; labels: ArrayRecord }[]; - paint_box: PaintCase[]; - prompts: { - max_prompt_chars: number; editable: Record; editable_generic: string; - cases: { id: string; profile: OrganelleProfile; resolution_nm: number | null; prompt_override: string | null; prompt?: string; error?: Refusal }[]; - }; - organelles: { catalog: OrganelleProfile[]; lookups: { name: string; key: string | null }[]; resolved: { name: string; profile: OrganelleProfile }[] }; -} - -const fixture = loadFixture("ai_annotate.json"); - -/** A fixture array as the labels' typed array (uint8 or uint16, the paint layer's dtypes). */ -function labelsOf(record: ArrayRecord): Labels { - if (record.dtype === "uint16") return Uint16Array.from(record.data); - if (record.dtype === "uint8") return Uint8Array.from(record.data); - throw new Error(`${record.dtype} is not a labels dtype`); -} - -const prod = (shape: number[]) => shape.reduce((a, b) => a * b, 1); - -/** Expect a refusal with the Python's category, message and status. */ -function expectRefused(fn: () => unknown, error: Refusal, label: string): void { - let caught: unknown; - try { - fn(); - } catch (e) { - caught = e; - } - expect(caught, `${label}: raised`).toBeInstanceOf(AIAnnotateError); - const e = caught as AIAnnotateError; - expect(e.category, `${label}: category`).toBe(error.category); - expect(e.httpStatus, `${label}: status`).toBe(error.http_status); - expect(e.userMessage, `${label}: message`).toBe(error.message); -} - -const planCases = new Map(fixture.plan_plane.map((c) => [c.id, c])); -const planOf = (id: string): PlanePlan => { - const c = planCases.get(id); - if (!c?.plan) throw new Error(`no plan ${id} in the fixture`); - return c.plan; -}; - -describe("constants", () => { - it("are the Python's", () => { - expect(MAX_IMAGE_SIDE).toBe(fixture.constants.max_image_side); - expect([...PLANE_NAMES]).toEqual(fixture.constants.plane_names); - expect(MAX_PROMPT_CHARS).toBe(fixture.prompts.max_prompt_chars); - }); -}); - -describe("screen layout (to_screen, from_screen, screen_shape)", () => { - for (const c of fixture.screen) { - it(`depth axis ${c.depth_axis}, display dimensions ${c.display_dimensions?.join(", ") ?? "unset"}`, () => { - const [rows, cols] = c.plane.shape; - const screen = toScreen(Int32Array.from(c.plane.data), rows, cols, c.depth_axis, c.display_dimensions); - expect([screen.rows, screen.cols]).toEqual(c.to_screen.shape); - expect(Array.from(screen.data)).toEqual(c.to_screen.data); - expect(screenShape([rows, cols], c.depth_axis, c.display_dimensions)).toEqual(c.screen_shape); - const [imageRows, imageCols] = c.image.shape; - const back = fromScreen(Int32Array.from(c.image.data), imageRows, imageCols, c.depth_axis, c.display_dimensions); - expect([back.rows, back.cols]).toEqual(c.from_screen.shape); - expect(Array.from(back.data)).toEqual(c.from_screen.data); - }); - } -}); - -describe("plan_plane", () => { - for (const c of fixture.plan_plane) { - it(c.id, () => { - const plan = () => planPlane(c.volume, c.volume_shape, c.point_nm, c.depth_axis, c.crop_size_px, c.data_box_nm, c.display_dimensions); - if (c.error) { - expectRefused(plan, c.error, c.id); - return; - } - const got = plan(), want = c.plan!; - expect(got.depth_axis).toBe(want.depth_axis); - expect(got.display_dimensions).toEqual(want.display_dimensions); - expect(got.image_shape).toEqual(want.image_shape); - expect(got.write_lo).toEqual(want.write_lo); - expect(got.write_hi).toEqual(want.write_hi); - for (const key of ["point_nm", "raw_offset_nm", "raw_shape_nm", "input_voxel_size"] as const) { - expect(got[key].length, key).toBe(3); - got[key].forEach((v, a) => expect(v, `${key}[${a}]`).toBeCloseTo(want[key][a], 9)); - } - expect(got.resolution_nm).toBeCloseTo(want.resolution_nm, 9); - expect(clickPx(got)).toEqual(c.click_px); - expect(planeAxes(got)).toEqual(c.plane_axes); - expect(writeShape(got)).toEqual(c.write_shape); - expect(planeName(got)).toBe(c.plane_name); - }); - } - - it("covers each depth axis, a refusal of each kind, a clipped field, anisotropic voxels and a half-pixel rounding", () => { - const ids = new Set(planCases.keys()); - for (const id of ["centred_z", "centred_y", "centred_x", "clipped_edge", "aniso_xz", "data_box_clip", "half_pixel_rounding"]) expect(ids, id).toContain(id); - for (const id of ["centred_z", "centred_y", "centred_x", "aniso_xz"]) expect(ids, `${id}_shown_xyz`).toContain(`${id}_shown_xyz`); - expect(planCases.get("outside_before_z")!.error!.message).toContain("outside the annotation volume"); - expect(planCases.get("data_box_refused")!.error!.message).toContain("outside the data"); - // 1001 voxels at 16 nm pixels is 500.5 px: numpy rounds it to 500. - expect(planOf("half_pixel_rounding").image_shape).toEqual([1024, 500]); - }); -}); - -describe("raw_box", () => { - for (const c of fixture.raw_box) { - it(`${c.plan} on a ${c.voxel_size.join("×")} nm grid from ${c.corner.join(", ")}`, () => { - const box = rawBox(planOf(c.plan), c.voxel_size, c.corner); - expect(box.begin).toEqual(c.begin); - expect(box.count).toEqual(c.count); - }); - } -}); - -describe("extract_mask", () => { - const { image } = fixture.extract_mask; - const [height, width] = image.shape; - const data = new Uint8ClampedArray(width * height * 4); - for (let i = 0; i < width * height; i++) { - data.set(image.data.slice(i * 3, i * 3 + 3), i * 4); - data[i * 4 + 3] = 255; - } - const rgba = { width, height, data }; - for (const c of fixture.extract_mask.cases) { - it(`${c.color_name} (${c.target_rgb.join(", ")}) at ${c.threshold}`, () => { - expectArrayMatches(extractMask(rgba, c.target_rgb, c.threshold), c.mask, c.color_name); - }); - } -}); - -describe("normalize_to_uint8", () => { - for (const c of fixture.normalize_to_uint8) { - it(c.id, () => { - const plane = c.plane.dtype === "float32" ? Float32Array.from(c.plane.data) : c.plane.dtype === "uint16" ? Uint16Array.from(c.plane.data) : Uint8Array.from(c.plane.data); - expectArrayMatches(normalizeToUint8(plane, c.plane.dtype === "uint8"), c.uint8, c.id); - }); - } -}); - -describe("_resize (PIL BOX when shrinking, BILINEAR otherwise)", () => { - for (const c of fixture.resize) { - it(c.id, () => { - const [rows, cols] = c.plane.shape; - const out = resizePlane({ rows, cols, data: Float32Array.from(c.plane.data) }, c.shape[0], c.shape[1]); - expect([out.rows, out.cols]).toEqual(c.shape); - expect(c.resized.dtype).toBe("float32"); - expectArrayMatches(out.data, c.resized, c.id); - }); - } -}); - -describe("mask_to_write_shape", () => { - for (const c of fixture.mask_to_write_shape) { - it(c.id, () => { - const plan = planOf(c.plan); - expect(c.mask.shape).toEqual(plan.image_shape); - const out = maskToWriteShape(Uint8Array.from(c.mask.data), plan); - expect(c.write.shape).toEqual(writeShape(plan)); - expectArrayMatches(out, c.write, c.id); - }); - } -}); - -describe("box_voxels and volume_corner_nm", () => { - for (const c of fixture.box_voxels) { - it(c.id, () => { - volumeCornerNm(c.volume.dataset_offset_nm, c.volume.output_voxel_size).forEach((v, a) => expect(v).toBeCloseTo(c.corner_nm[a], 9)); - expect(boxVoxels(c.volume, c.offset_nm, c.shape_nm, c.volume_shape)).toEqual(c.box); - }); - } -}); - -describe("_fresh_ids", () => { - for (const c of fixture.fresh_ids) { - it(c.id, () => { - const existing = labelsOf(c.existing); - if (c.error !== undefined) { - expectRefused(() => freshIds(existing, c.count, c.count_up), { category: "refused", message: c.error, http_status: 409 }, c.id); - return; - } - expect(freshIds(existing, c.count, c.count_up)).toEqual(c.ids); - }); - } -}); - -describe("seed_labels", () => { - for (const c of fixture.seed_labels) { - it(c.id, () => { - const existing = labelsOf(c.existing); - const objects = Int32Array.from(c.objects.data); - if (c.error !== undefined) { - expectRefused(() => seedLabels(objects, existing, c.count_up), { category: "refused", message: c.error, http_status: 409 }, c.id); - return; - } - const labels = seedLabels(objects, existing, c.count_up); - expect(labels.constructor).toBe(existing.constructor); - expectArrayMatches(labels, c.labels!, c.id); - }); - } -}); - -describe("labels_for_box", () => { - for (const c of fixture.labels_for_box) { - it(c.id, () => { - // The box is one voxel thick along the depth axis, so its C-order - // layout is the plane's: writeLo 0, writeHi the box's shape. - const existing = labelsOf(c.existing); - const beside = (c.beside ?? []).map((p) => (p ? labelsOf(p) : null)); - const labels = labelsForBox(Int32Array.from(c.objects.data), existing, [0, 0, 0], c.existing.shape, c.depth_axis, c.count_up, beside); - expect(labels.constructor).toBe(existing.constructor); - expectArrayMatches(labels, c.labels, c.id); - }); - } -}); - -describe("paint_box and restore_box", () => { - /** A paint volume of the case's shape and chunks in a fresh store, its - * nonzero chunks written as the brush would have written them (a chunk - * never painted has no file, and reads as zeros). */ - async function storeOf(c: PaintCase): Promise<{ store: MemoryPaintStore; volume: PaintVolume }> { - const store = new MemoryPaintStore(); - const volume: PaintVolume = { - id: "vol-1", type: "annotation_volume", model_name: "m", model_id: "m", dataset_path: "x", chunk_size: c.chunks, input_size: c.chunks, - output_voxel_size: [8, 8, 8], input_voxel_size: [8, 8, 8], dataset_offset_nm: [4, 4, 4], dataset_shape_voxels: c.shape, dtype: c.dtype, - input_norm: [], postprocess: [], created_at: "2026-10-15T12:00:00Z", - }; - const initial = labelsOf(c.initial); - const [nz, ny, nx] = c.shape, [cz, cy, cx] = c.chunks; - for (let iz = 0; iz < nz / cz; iz++) for (let iy = 0; iy < ny / cy; iy++) for (let ix = 0; ix < nx / cx; ix++) { - const chunk = c.dtype === "uint16" ? new Uint16Array(cz * cy * cx) : new Uint8Array(cz * cy * cx); - let any = false; - for (let z = 0; z < cz; z++) for (let y = 0; y < cy; y++) for (let x = 0; x < cx; x++) { - const v = initial[((iz * cz + z) * ny + iy * cy + y) * nx + ix * cx + x]; - chunk[(z * cy + y) * cx + x] = v; - if (v) any = true; - } - if (any) await store.put(volume.id, CHUNK_DIR + chunkKey([iz, iy, ix]), chunkBytes(chunk)); - } - return { store, volume }; - } - - for (const c of fixture.paint_box) { - it(c.id, async () => { - const { store, volume } = await storeOf(c); - const labels = labelsOf(c.labels); - const undo: UndoEntry[] = []; - const counts = await paintBox(store, volume, c.lo, c.hi, () => labels.slice(), { overwrite: c.overwrite, undo }); - expect(counts).toEqual(c.counts); - const whole = [0, 0, 0]; - expectArrayMatches(await readBox(store, volume, whole, c.shape), c.after, `${c.id}: after`); - if (!c.undo) { - expect(undo).toEqual([]); - // Nothing written: no chunk file appeared. - const files = await store.list(volume.id, CHUNK_DIR); - const initial = labelsOf(c.initial); - const { store: fresh } = await storeOf(c); - expect(files).toEqual(await fresh.list(volume.id, CHUNK_DIR)); - expectArrayMatches(await readBox(store, volume, whole, c.shape), c.initial, `${c.id}: unchanged`); - expect(initial.length).toBe(prod(c.shape)); - } else { - expect(undo.length).toBe(1); - expect(undo[0].lo).toEqual(c.lo); - expect(undo[0].hi).toEqual(c.hi); - expectArrayMatches(undo[0].before, c.undo.before, `${c.id}: undo before`); - expectArrayMatches(undo[0].after, c.undo.after, `${c.id}: undo after`); - } - if (c.restore) { - for (const [index, value] of c.restore.stroke) { - const one = c.dtype === "uint16" ? Uint16Array.of(value) : Uint8Array.of(value); - await writeBox(store, volume, index, index.map((i) => i + 1), one); - } - expect(await restoreBox(store, volume, undo[0])).toBe(c.restore.restored); - expectArrayMatches(await readBox(store, volume, whole, c.shape), c.restore.final, `${c.id}: restored`); - } - }); - } -}); - -describe("prompts", () => { - it("editable_prompt, per catalog entry and for a generic profile", () => { - for (const [key, text] of Object.entries(fixture.prompts.editable)) expect(editablePrompt(ORGANELLES[key]), key).toBe(text); - expect(editablePrompt(resolveOrganelleProfile("golgi"))).toBe(fixture.prompts.editable_generic); - }); - for (const c of fixture.prompts.cases) { - it(`build_recolor_prompt: ${c.id}`, () => { - const build = () => buildRecolorPrompt(c.profile, c.resolution_nm, c.prompt_override); - if (c.error) expectRefused(build, c.error, c.id); - else expect(build()).toBe(c.prompt); - }); - } -}); - -describe("organelles", () => { - it("the catalog, verbatim and in order", () => { - expect(Object.values(ORGANELLES)).toEqual(fixture.organelles.catalog); - }); - for (const l of fixture.organelles.lookups) { - it(`find_organelle_profile(${JSON.stringify(l.name)})`, () => { - expect(findOrganelleProfile(l.name)?.key ?? null).toBe(l.key); - }); - } - for (const r of fixture.organelles.resolved) { - it(`resolve_organelle_profile(${JSON.stringify(r.name)})`, () => { - expect(resolveOrganelleProfile(r.name)).toEqual(r.profile); - }); - } -}); diff --git a/web/test/parity/label-it.test.ts b/web/test/parity/label-it.test.ts index f608d0ee..8db45139 100644 --- a/web/test/parity/label-it.test.ts +++ b/web/test/parity/label-it.test.ts @@ -1,203 +1,43 @@ -// Parity of the browser's Label it tools with the server's: -// web/src/label-it (segment.ts, prediction.ts) with -// cellmap_flow/dashboard/routes/finetune/view_labels.py (probability, -// segment_prediction, read_prediction), and the Label it parts of -// web/src/ai-annotate (labels.ts relabelObjects, fill.ts rewriteForeground) -// with cellmap_flow/finetune/session/fill.py (relabel_objects, -// rewrite_foreground, restore_box). +// Parity of the browser's Label it tools with the server's, where the +// browser runs TypeScript of its own: a seed's mutex watershed +// (web/src/label-it/segment.ts mutexObjects) with +// cellmap_flow/dashboard/routes/finetune/view_labels.py's +// (segment_prediction, on mwatershed). The rest of a seed, and the ids, +// writes and undo of every Label it tool, are the Python both modes run +// (cellmap_flow/portable/label_it.py and fill.py), tested by pytest. // // The fixture (test/fixtures/label_it.json) was written by the Python -// functions themselves (test/fixtures/make_label_it_fixtures.py, whose -// docstring lists the sections): predictions of every output class turned -// into probabilities and segmented into objects by components and the mutex -// watershed; label boxes relabelled by Split Objects' rule, in memory and in -// a chunked volume with its undo; and a served array read under annotation -// boxes on other grids. Objects, ids and boxes match exactly, probabilities -// within the parity tolerance (scipy's float32 expit and the port's agree to -// an ulp). Regenerate the fixture when the Python changes; -// tests/web/test_fixtures_fresh.py fails when it is stale. +// functions themselves (test/fixtures/make_label_it_fixtures.py): affinities, +// as the browser's Python hands them to the watershed, segmented into +// objects. Objects match exactly. Regenerate the fixture when the Python +// changes; tests/web/test_fixtures_fresh.py fails when it is stale. import { describe, expect, it } from "vitest"; -import { chunkBytes, readBox, restoreBox, rewriteForeground, writeBox } from "../../src/ai-annotate/fill"; -import { relabelObjects } from "../../src/ai-annotate/labels"; -import { AIAnnotateError, type Labels, type UndoEntry } from "../../src/ai-annotate/types"; -import type { PaintVolume } from "../../src/contracts"; -import { CHUNK_DIR, chunkKey } from "../../src/finetune/volume"; -import { boxFromChunks } from "../../src/infer/output"; -import { readPrediction } from "../../src/label-it/prediction"; -import { type Block, probability, segmentPrediction } from "../../src/label-it/segment"; -import { MemoryPaintStore } from "../../src/state/paint-store"; +import { mutexObjects } from "../../src/label-it/segment"; import { type ArrayRecord, expectArrayMatches, loadFixture } from "./helpers"; -type Counts = { objects: number; split: number; merged: number }; interface Fixture { seed: number; - blocks: Record; - probability: { id: string; block: string; output_class: string | null; channels: number[] | null; probability: ArrayRecord }[]; - segment: { - id: string; method: string; block: string; output_class: string | null; channels: number[] | null; offsets: number[][] | null; - threshold: number; min_size: number; connectivity: number; per_slice: boolean; objects: ArrayRecord; count: number; - }[]; - relabel_objects: { id: string; existing: ArrayRecord; connectivity: number; per_slice: boolean; labels?: ArrayRecord; counts?: Counts; error?: string }[]; - rewrite_foreground: { - id: string; dtype: "uint8" | "uint16"; shape: number[]; chunks: number[]; initial: ArrayRecord; lo: number[]; hi: number[]; - connectivity: number; per_slice: boolean; changed: number; counts: Counts; after: ArrayRecord; - undo: { before: ArrayRecord; after: ArrayRecord } | null; - restore?: { stroke: [number[], number][]; restored: number; final: ArrayRecord }; - }[]; - read_prediction: { - id: string; served: { shape: number[]; voxel: number[]; translation: number[]; chunks: number[] }; - volume: { output_voxel_size: number[]; dataset_offset_nm: number[] }; lo: number[]; hi: number[]; - prediction?: ArrayRecord; new_lo?: number[]; new_hi?: number[]; error?: { message: string; status: number }; + mutex_watershed: { + id: string; output_class: string | null; offsets: number[][]; threshold: number; min_size: number; + affinities: ArrayRecord; objects: ArrayRecord; count: number; }[]; } const fixture = loadFixture("label_it.json"); -function blockOf(name: string): Block { - const record = fixture.blocks[name]; - return { data: Float32Array.from(record.data), shape: record.shape }; -} - -/** A fixture array as the labels' typed array (uint8 or uint16, the paint layer's dtypes). */ -function labelsOf(record: ArrayRecord): Labels { - if (record.dtype === "uint16") return Uint16Array.from(record.data); - if (record.dtype === "uint8") return Uint8Array.from(record.data); - throw new Error(`${record.dtype} is not a labels dtype`); -} - -describe("probability", () => { - for (const c of fixture.probability) { +describe("mutex_watershed", () => { + for (const c of fixture.mutex_watershed) { it(c.id, () => { - const out = probability(blockOf(c.block), c.output_class, c.channels); - expect(out.shape).toEqual(c.probability.shape); - expectArrayMatches(out.data, c.probability, c.id); - }); - } -}); - -describe("segment_prediction", () => { - for (const c of fixture.segment) { - it(`${c.id} (${c.method})`, () => { - const objects = segmentPrediction(c.method, blockOf(c.block), c.output_class, c.channels, c.offsets, { - threshold: c.threshold, minSize: c.min_size, connectivity: c.connectivity, perSlice: c.per_slice, - }); + const affinities = { data: Float32Array.from(c.affinities.data), shape: c.affinities.shape }; + const objects = mutexObjects(affinities, c.offsets, c.threshold, c.min_size); expectArrayMatches(objects, c.objects, c.id); expect(Math.max(0, ...objects)).toBe(c.count); }); } - it("covers every method the browser offers, with objects to find", () => { - expect(new Set(fixture.segment.map((c) => c.method))).toEqual(new Set(["components", "mutex_watershed"])); - expect(fixture.segment.some((c) => c.method === "mutex_watershed" && c.count > 2)).toBe(true); - expect(fixture.segment.some((c) => c.per_slice && c.count > 10)).toBe(true); + it("has objects to find, and cases that drop some by size", () => { + expect(fixture.mutex_watershed.some((c) => c.count > 2)).toBe(true); + expect(fixture.mutex_watershed.some((c) => c.min_size > 1)).toBe(true); }); }); - -describe("relabel_objects", () => { - for (const c of fixture.relabel_objects) { - it(c.id, () => { - const existing = labelsOf(c.existing); - const run = () => relabelObjects(existing, c.existing.shape, { connectivity: c.connectivity, perSlice: c.per_slice }); - if (c.error) { - expect(run).toThrow(AIAnnotateError); - expect(run).toThrow(c.error); - return; - } - const { labels, counts } = run(); - expect(labels.constructor).toBe(existing.constructor); - expectArrayMatches(labels, c.labels!, c.id); - expect(counts).toEqual(c.counts); - }); - } -}); - -describe("rewrite_foreground and restore_box", () => { - /** A paint volume of the case's shape and chunks in a fresh store, its - * nonzero chunks written as the brush would have written them. */ - async function storeOf(c: Fixture["rewrite_foreground"][number]): Promise<{ store: MemoryPaintStore; volume: PaintVolume }> { - const store = new MemoryPaintStore(); - const volume: PaintVolume = { - id: "vol-1", type: "annotation_volume", model_name: "m", model_id: "m", dataset_path: "x", chunk_size: c.chunks, input_size: c.chunks, - output_voxel_size: [8, 8, 8], input_voxel_size: [8, 8, 8], dataset_offset_nm: [4, 4, 4], dataset_shape_voxels: c.shape, dtype: c.dtype, - input_norm: [], postprocess: [], created_at: "2026-10-16T12:00:00Z", - }; - const initial = labelsOf(c.initial); - const [nz, ny, nx] = c.shape, [cz, cy, cx] = c.chunks; - for (let iz = 0; iz < nz / cz; iz++) for (let iy = 0; iy < ny / cy; iy++) for (let ix = 0; ix < nx / cx; ix++) { - const chunk = c.dtype === "uint16" ? new Uint16Array(cz * cy * cx) : new Uint8Array(cz * cy * cx); - let any = false; - for (let z = 0; z < cz; z++) for (let y = 0; y < cy; y++) for (let x = 0; x < cx; x++) { - const v = initial[((iz * cz + z) * ny + iy * cy + y) * nx + ix * cx + x]; - chunk[(z * cy + y) * cx + x] = v; - if (v) any = true; - } - if (any) await store.put(volume.id, CHUNK_DIR + chunkKey([iz, iy, ix]), chunkBytes(chunk)); - } - return { store, volume }; - } - - for (const c of fixture.rewrite_foreground) { - it(c.id, async () => { - const { store, volume } = await storeOf(c); - const undo: UndoEntry[] = []; - const shape = c.hi.map((h, a) => h - c.lo[a]); - const result = await rewriteForeground(store, volume, c.lo, c.hi, (existing) => relabelObjects(existing, shape, { connectivity: c.connectivity, perSlice: c.per_slice }), { undo }); - expect(result).toEqual({ changed: c.changed, counts: c.counts }); - const whole = [0, 0, 0]; - expectArrayMatches(await readBox(store, volume, whole, c.shape), c.after, `${c.id}: after`); - if (!c.undo) { - expect(undo).toEqual([]); - return; - } - expect(undo.length).toBe(1); - expect([undo[0].lo, undo[0].hi]).toEqual([c.lo, c.hi]); - expectArrayMatches(undo[0].before, c.undo.before, `${c.id}: undo before`); - expectArrayMatches(undo[0].after, c.undo.after, `${c.id}: undo after`); - if (c.restore) { - for (const [index, value] of c.restore.stroke) { - const one = c.dtype === "uint16" ? Uint16Array.of(value) : Uint8Array.of(value); - await writeBox(store, volume, index, index.map((i) => i + 1), one); - } - expect(await restoreBox(store, volume, undo[0])).toBe(c.restore.restored); - expectArrayMatches(await readBox(store, volume, whole, c.shape), c.restore.final, `${c.id}: restored`); - } - }); - } -}); - -describe("read_prediction", () => { - /** The served array's value at (c, z, y, x): make_label_it_fixtures' _served_values. */ - const valueAt = (c: number, z: number, y: number, x: number) => c * 1_000_000 + z * 10_000 + y * 100 + x; - - for (const c of fixture.read_prediction) { - it(c.id, async () => { - const [C, ...levelShape] = c.served.shape; - const chunk = c.served.chunks; - // The model's output as the engine serves it: whole chunks, the part - // past the array zero (its fill), assembled into the box asked for. - const infer = { - readOutput: (_view: string, lo: number[], hi: number[]) => - boxFromChunks(lo, hi, chunk, C, async (index) => { - const values = new Float32Array(C * chunk[0] * chunk[1] * chunk[2]); - let i = 0; - for (let ch = 0; ch < C; ch++) for (let z = 0; z < chunk[0]; z++) for (let y = 0; y < chunk[1]; y++) for (let x = 0; x < chunk[2]; x++, i++) { - const at = [index[0] * chunk[0] + z, index[1] * chunk[1] + y, index[2] * chunk[2] + x]; - if (at.every((v, a) => v < levelShape[a])) values[i] = valueAt(ch, at[0], at[1], at[2]); - } - return values; - }), - }; - const level = { shape: levelShape, voxel: c.served.voxel, origin: c.served.translation }; - const read = await readPrediction(infer, "view", level, c.volume, c.lo, c.hi); - if (c.error) { - expect(read).toBeNull(); - return; - } - expect(read).not.toBeNull(); - expect([read!.lo, read!.hi]).toEqual([c.new_lo, c.new_hi]); - expect(read!.prediction.shape).toEqual(c.prediction!.shape); - expectArrayMatches(read!.prediction.data, c.prediction!, c.id); - }); - } -}); diff --git a/web/test/parity/targets.test.ts b/web/test/parity/targets.test.ts deleted file mode 100644 index dbad13f3..00000000 --- a/web/test/parity/targets.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Parity of web/src/finetune/targets.ts with cellmap_flow/finetune/ -// target_transforms.py (BinaryTargetTransform, BroadcastBinaryTargetTransform, -// AffinityTargetTransform). -// -// The fixture (test/fixtures/targets.json) was written by the Python classes -// themselves (test/fixtures/make_fixtures.py): label blocks [z, y, x] with the -// paint layer's values (0 unpainted, 1 background, 2+ objects; a one-slice -// block, a fully painted one, one with uint16 ids, one all zero), and for -// each case the class with its constructor arguments and the target and -// mask it returned, float32 [C, z, y, x] with the batch axis dropped, which -// is the port's channels × N layout. Targets are 0 or 1, so the match is -// exact. Regenerate the fixture when the Python changes; -// tests/web/test_fixtures_fresh.py fails when it is stale. -import { describe, expect, it } from "vitest"; - -import { affinityTargets, binaryTargets } from "../../src/finetune/targets"; -import type { PaintedChunk, TargetTransform } from "../../src/finetune/types"; -import { type ArrayRecord, expectArrayMatches, loadFixture } from "./helpers"; - -interface TargetCase { - id: string; - transform: "BinaryTargetTransform" | "BroadcastBinaryTargetTransform" | "AffinityTargetTransform"; - params: { num_channels?: number; offsets?: number[][] }; - input: string; - target: ArrayRecord; - mask: ArrayRecord; -} - -interface TargetsFixture { - seed: number; - inputs: Record; - cases: TargetCase[]; -} - -const fixture = loadFixture("targets.json"); - -/** The port of the case's class, built as the Python was. */ -function transformOf(c: TargetCase): TargetTransform { - switch (c.transform) { - case "BinaryTargetTransform": - return binaryTargets(1); - case "BroadcastBinaryTargetTransform": - return binaryTargets(c.params.num_channels!); - case "AffinityTargetTransform": - return affinityTargets(c.params.offsets!, c.params.num_channels!); - } -} - -/** A fixture block as the painted chunk the port takes: its labels in the - * volume's dtype (uint8, or uint16 for an instance volume's ids). */ -function chunkOf(record: ArrayRecord): PaintedChunk { - const labels = record.dtype === "uint16" ? Uint16Array.from(record.data) : Uint8Array.from(record.data); - return { index: [0, 0, 0], shape: record.shape.slice(), labels }; -} - -const sum = (values: ArrayLike, from = 0, to = values.length) => { - let s = 0; - for (let i = from; i < to; i++) s += values[i]; - return s; -}; - -describe("the target transforms match the Python classes", () => { - it("has label blocks in the paint layer's dtypes", () => { - for (const [name, block] of Object.entries(fixture.inputs)) { - expect(["uint8", "uint16"], `${name}: dtype`).toContain(block.dtype); - expect(block.shape.length, `${name}: axes`).toBe(3); - expect(block.data.length, `${name}: voxels`).toBe(block.shape.reduce((a, b) => a * b, 1)); - } - }); - - for (const c of fixture.cases) { - it(c.id, () => { - const block = fixture.inputs[c.input]; - const out = transformOf(c)(chunkOf(block)); - const n = block.data.length; - expect(c.target.dtype, "the Python returned float32").toBe("float32"); - expect(c.target.shape.slice(1), "the Python's target covers the block").toEqual(block.shape); - expect(out.channels, "channels").toBe(c.target.shape[0]); - expect(out.target.length, "target length").toBe(out.channels * n); - expect(out.mask.length, "mask length").toBe(out.channels * n); - expectArrayMatches(out.target, c.target, `${c.id}: target`); - expectArrayMatches(out.mask, c.mask, `${c.id}: mask`); - }); - } - - it("covers what the port must get right: an empty z mask on a one-slice chunk, a masked-out extra channel, both affinity values", () => { - const byId = new Map(fixture.cases.map((c) => [c.id, c])); - // A 2D student's chunk is one slice: the z offset pairs nothing, so its - // channel has no mask (and its head keeps the model's weights); the y - // and x channels do. - const slice = byId.get("affinity_3_slice_1_6_7")!; - const n = fixture.inputs[slice.input].data.length; - expect(sum(slice.mask.data, 0, n)).toBe(0); - expect(sum(slice.mask.data, n, 2 * n)).toBeGreaterThan(0); - expect(sum(slice.mask.data, 2 * n, 3 * n)).toBeGreaterThan(0); - // num_channels past the offsets: the fourth channel is masked out. - const four = byId.get("affinity_4_block_4_5_6")!; - const m = fixture.inputs[four.input].data.length; - expect(four.target.shape[0]).toBe(4); - expect(sum(four.mask.data, 3 * m, 4 * m)).toBe(0); - expect(sum(four.target.data, 3 * m, 4 * m)).toBe(0); - // Masked affinities of both values: same object (1) and different (0). - const masked = four.mask.data.map((v, i) => (v ? four.target.data[i] : -1)); - expect(masked).toContain(1); - expect(masked).toContain(0); - // Nothing painted: nothing in any mask. - for (const c of fixture.cases) if (c.input.startsWith("zeros")) expect(sum(c.mask.data), c.id).toBe(0); - // The one-channel binary target is the broadcast's first channel. - const binary = byId.get("binary_block_4_5_6")!, broadcast = byId.get("broadcast_3_block_4_5_6")!; - expect(broadcast.target.data.slice(0, m)).toEqual(binary.target.data); - expect(broadcast.target.data.slice(2 * m, 3 * m)).toEqual(binary.target.data); - }); -}); diff --git a/web/test/unit/ai-annotate-gemini.test.ts b/web/test/unit/ai-annotate-gemini.test.ts index 85307f58..789eb7fb 100644 --- a/web/test/unit/ai-annotate-gemini.test.ts +++ b/web/test/unit/ai-annotate-gemini.test.ts @@ -1,8 +1,10 @@ // gemini.ts against a scripted fetch: the request's URL, headers and JSON for // both endpoints, the answer parsed (thought parts skipped, the last image -// taken, the padding cropped, another size resized), the retries and their -// waits, the failures' categories and statuses, and that no message ever -// carries the key, the token or the service's body. The Vertex cases follow +// taken, the padding cropped, another size resized: the mask is the portable +// Python's, here in node's Pyodide, test/unit/pyodide.ts, so the cases that +// read one skip without it), the retries and their waits, the failures' +// categories and statuses, and that no message ever carries the key, the +// token or the service's body. The Vertex cases follow // tests/ai_annotate/test_backends.py's; the Gemini API has its own // (listModels, the key header, API_KEY_INVALID). import { afterEach, describe, expect, it, vi } from "vitest"; @@ -13,6 +15,11 @@ import { } from "../../src/ai-annotate/gemini"; import { jsCodec, PNG_SIGNATURE } from "../../src/ai-annotate/images"; import { AIAnnotateError, CALL_TIMEOUT_MS, type Provider, type RgbaImage, type SegmentRequest } from "../../src/ai-annotate/types"; +import { portablePython } from "./pyodide"; + +// Pyodide and numpy load once for the file, a few seconds. +vi.setConfig({ testTimeout: 120_000 }); +const python = await portablePython(); const codec = jsCodec(); const RED: [number, number, number] = [255, 0, 0]; @@ -102,8 +109,10 @@ class FakeFetch { }; } +/** The provider for `type` over `fake`, its segment handed the portable Python as the job hands it. */ function provider(type: GeminiType, fake: FakeFetch, sleeps: number[] = []): Provider { - return createGeminiProvider(type, { fetch: fake.fetch, codec, sleep: async (ms) => { sleeps.push(ms); } }); + const made = createGeminiProvider(type, { fetch: fake.fetch, codec, sleep: async (ms) => { sleeps.push(ms); } }); + return { ...made, segment: (req, model, credentials, options = {}) => made.segment(req, model, credentials, { python: python ?? undefined, ...options }) }; } const GEMINI_CREDENTIALS = { api_key: KEY }; @@ -132,7 +141,7 @@ afterEach(() => { // --------------------------------------------------------------------------- describe("the request", () => { - it("sends the plane padded square as PNG with the prompt to the Gemini API with the key header, and reads the mask", async () => { + it.skipIf(!python)("sends the plane padded square as PNG with the prompt to the Gemini API with the key header, and reads the mask", async () => { const draft = image(60, 60, () => RED); // a thought image: all red, which would mask everything if taken const fake = new FakeFetch([async () => json(200, answer([ await imagePart(draft, "image/png", { thought: true }), @@ -172,7 +181,7 @@ describe("the request", () => { expect(result.usage).toEqual({ prompt_tokens: 600, output_tokens: 1300, total_tokens: 1900 }); }); - it("sends to Vertex AI with the project, the location (global unless given) and the bearer token", async () => { + it.skipIf(!python)("sends to Vertex AI with the project, the location (global unless given) and the bearer token", async () => { const fake = new FakeFetch([async () => json(200, answer([await imagePart(paintedSquare())])), async () => json(200, answer([await imagePart(paintedSquare())]))]); const vertex = provider("vertex_gemini", fake); await vertex.segment(request(), "gemini-3-pro-image", VERTEX_CREDENTIALS); @@ -211,7 +220,7 @@ describe("the answer", () => { expect(error.userMessage).not.toContain("x".repeat(201)); }); - it("resizes an image of another size back to the sent size, then crops the padding off", async () => { + it.skipIf(!python)("resizes an image of another size back to the sent size, then crops the padding off", async () => { const fake = new FakeFetch([ async () => json(200, answer([await imagePart(paintedSquare(120))])), async () => json(200, answer([await imagePart(image(120, 80, (r, c) => (r >= 20 && r < 40 && c >= 40 && c < 70 ? RED : [0, 0, 0])))])), @@ -228,7 +237,7 @@ describe("the answer", () => { expect(maskCount(stretched.mask)).toBeGreaterThan(0); }); - it("decodes by the bytes when the model declares the wrong type, and refuses what is not an image", async () => { + it.skipIf(!python)("decodes by the bytes when the model declares the wrong type, and refuses what is not an image", async () => { const gifBytes = Uint8Array.from("GIF89a-not-an-image".split(""), (ch) => ch.charCodeAt(0)); const huge = new Uint8Array(20); huge.set(PNG_SIGNATURE, 0); @@ -258,7 +267,7 @@ describe("the answer", () => { }); describe("retries", () => { - it("retries a 429 and a 503 with the bounded waits, then succeeds", async () => { + it.skipIf(!python)("retries a 429 and a 503 with the bounded waits, then succeeds", async () => { const sleeps: number[] = []; const fake = new FakeFetch([ async () => apiError(429, "RESOURCE_EXHAUSTED"), diff --git a/web/test/unit/ai-annotate-geometry.test.ts b/web/test/unit/ai-annotate-geometry.test.ts deleted file mode 100644 index 2f3107e7..00000000 --- a/web/test/unit/ai-annotate-geometry.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Planning the plane an AI annotation reads and the voxels it writes -// (src/ai-annotate/geometry.ts) against cellmap_flow/ai_annotate/geometry.py, -// with the arithmetic done by hand as tests/ai_annotate/test_geometry.py does -// it. The volumes here have their voxel 0's corner at the world origin: an -// 8 nm volume's dataset_offset_nm is 4 (voxel 0's centre), so annotation -// voxel i covers [8i, 8i + 8) nm and the expectations are plain. The parity -// fixture (test/parity/ai-annotate.test.ts) checks the same functions -// against the Python's own numbers. -import { describe, expect, it } from "vitest"; - -import { clickPx, planeAxes, planeName, planPlane, writeShape } from "../../src/ai-annotate/geometry"; -import { AIAnnotateError, type DepthAxis } from "../../src/ai-annotate/types"; - -function volume(output = [8, 8, 8], input = [4, 4, 4], offset?: number[]) { - return { output_voxel_size: output, input_voxel_size: input, dataset_offset_nm: offset ?? output.map((v) => v / 2) }; -} - -const SHAPE = [100, 200, 300]; -const AXES: DepthAxis[] = [0, 1, 2]; - -describe("planPlane", () => { - const point = [50 * 8 + 1, 100 * 8 + 3, 150 * 8 + 6]; // in voxel (50, 100, 150), off its centre - - for (const depthAxis of AXES) { - it(`centres a plane on the point, one voxel thick along axis ${depthAxis}`, () => { - const plan = planPlane(volume(), SHAPE, point, depthAxis, 64); - const voxel = [50, 100, 150]; - for (let a = 0; a < 3; a++) { - if (a === depthAxis) { - expect([plan.write_lo[a], plan.write_hi[a]]).toEqual([voxel[a], voxel[a] + 1]); - } else { - // 64 voxels whose centres lie within 256 nm of the point. - expect(plan.write_hi[a] - plan.write_lo[a]).toBe(64); - for (let i = plan.write_lo[a]; i < plan.write_hi[a]; i++) expect(Math.abs((i + 0.5) * 8 - point[a])).toBeLessThanOrEqual(256); - } - } - expect(writeShape(plan)).toEqual([64, 64]); - // The raw is the same area at 4 nm, one input voxel thick at the point. - for (const a of planeAxes(plan)) { - expect(plan.raw_offset_nm[a]).toBe(plan.write_lo[a] * 8); - expect(plan.raw_shape_nm[a]).toBe(64 * 8); - } - expect(plan.raw_shape_nm[depthAxis]).toBe(4); - expect(plan.raw_offset_nm[depthAxis] + 2).toBe(point[depthAxis]); - expect(plan.image_shape).toEqual([128, 128]); - expect(plan.resolution_nm).toBe(4); - expect(plan.input_voxel_size).toEqual([4, 4, 4]); - expect(plan.point_nm).toEqual(point); - expect(planeName(plan)).toBe(["XY", "XZ", "YZ"][depthAxis]); - // The point's pixel: 4 nm pixels from the read's corner, laid out as the - // panel draws the plane (rows, cols): y, x with x leftwards; x, z; y, z. - const [down, across] = { 0: [1, 2], 1: [2, 0], 2: [1, 0] }[depthAxis]; - const row = Math.floor((point[down] - plan.raw_offset_nm[down]) / 4), col = Math.floor((point[across] - plan.raw_offset_nm[across]) / 4); - expect(clickPx(plan)).toEqual([row, depthAxis === 0 ? 128 - 1 - col : col]); - }); - } - - it("the volume's offset is voxel 0's centre", () => { - // dataset_offset_nm 100: voxel 0 covers [96, 104), so 103 nm is in voxel 0 and 104 nm in voxel 1. - const plan = planPlane(volume([8, 8, 8], [4, 4, 4], [100, 100, 100]), SHAPE, [103, 104 + 8 * 20, 104 + 8 * 30], 0, 4); - expect(plan.write_lo).toEqual([0, 19, 29]); - expect(plan.write_hi).toEqual([1, 23, 33]); - expect(plan.raw_offset_nm.slice(1)).toEqual([96 + 19 * 8, 96 + 29 * 8]); - }); - - it("anisotropic voxels give a square field and square pixels", () => { - // 16 nm z, 8 nm in y and x; the input at half each. - const v = volume([16, 8, 8], [8, 4, 4]); - const p = [50 * 16 + 8, 100 * 8 + 4, 150 * 8 + 4]; - const xz = planPlane(v, SHAPE, p, 1, 64); - // 64 of the finer (x) voxels is 512 nm; that is 32 of the 16 nm z voxels. - expect(writeShape(xz)).toEqual([32, 64]); - expect(xz.raw_shape_nm).toEqual([512, 4, 512]); - // Pixels are the finer input voxel size on both axes: the 8 nm z is drawn at 4. - expect(xz.image_shape).toEqual([128, 128]); - expect(xz.resolution_nm).toBe(4); - const xy = planPlane(v, SHAPE, p, 0, 64); - expect(writeShape(xy)).toEqual([64, 64]); - expect(xy.raw_shape_nm).toEqual([8, 512, 512]); - expect(xy.write_hi[0] - xy.write_lo[0]).toBe(1); - }); - - it("the write box is clipped at the volume edge, and the raw read with it", () => { - // Voxel (50, 3, 297): 3 voxels from y's start, 2 from x's end. - const plan = planPlane(volume(), SHAPE, [50 * 8 + 4, 3 * 8 + 4, 297 * 8 + 4], 0, 64); - expect(plan.write_lo).toEqual([50, 0, 297 - 32]); - expect(plan.write_hi).toEqual([51, 3 + 32, 300]); - expect(writeShape(plan)).toEqual([35, 35]); - expect(plan.raw_offset_nm.slice(1)).toEqual([0, (297 - 32) * 8]); - expect(plan.raw_shape_nm.slice(1)).toEqual([35 * 8, 35 * 8]); - expect(plan.image_shape).toEqual([70, 70]); - // x runs leftwards in the XY plane, as Neuroglancer draws it. - expect(clickPx(plan)).toEqual([Math.floor((3 * 8 + 4) / 4), 70 - 1 - Math.floor((32 * 8 + 4) / 4)]); - }); - - it("the field is clipped to the data's box too, and a point outside the data or the volume is refused", () => { - // The data ends at x = 1210 nm; the volume runs on to 2400. - const plan = planPlane(volume(), SHAPE, point, 0, 64, [[0, 0, 0], [800, 1600, 1210]]); - expect(plan.write_lo[2]).toBe(Math.ceil((1206 - 256) / 8 - 0.5)); - expect(plan.write_hi[2]).toBe(Math.ceil(1210 / 8 - 0.5)); - expect(plan.raw_offset_nm[2] + plan.raw_shape_nm[2]).toBe(plan.write_hi[2] * 8); - const refusals = [ - () => planPlane(volume(), SHAPE, point, 0, 64, [[0, 0, 0], [800, 1600, 1200]]), // past the data's end in x - () => planPlane(volume(), SHAPE, [-1, 800, 1200], 0, 64), // before the volume in z - () => planPlane(volume(), SHAPE, [400, 800, 300 * 8], 0, 64), // at the volume's end in x - ]; - for (const [i, refused] of refusals.entries()) { - let caught: unknown; - try { - refused(); - } catch (e) { - caught = e; - } - expect(caught, `case ${i}`).toBeInstanceOf(AIAnnotateError); - expect((caught as AIAnnotateError).category).toBe("refused"); - expect((caught as AIAnnotateError).httpStatus).toBe(400); - } - }); -}); diff --git a/web/test/unit/ai-annotate-mask.test.ts b/web/test/unit/ai-annotate-mask.test.ts deleted file mode 100644 index 6d102361..00000000 --- a/web/test/unit/ai-annotate-mask.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// From a model's reply to a mask (src/ai-annotate/mask.ts extractMask) -// against cellmap_flow/ai_annotate/mask_extraction.py. The parity fixture -// checks extractMask and normalizeToUint8 against the Python's own numbers; -// here the colour rule is spelled out. -import { describe, expect, it } from "vitest"; - -import { extractMask } from "../../src/ai-annotate/mask"; -import type { RgbaImage } from "../../src/ai-annotate/types"; - -/** An RGBA image of the given pixels (rows first), alpha 255. */ -function image(width: number, height: number, pixels: [number, number, number][]): RgbaImage { - const data = new Uint8ClampedArray(width * height * 4); - pixels.forEach((rgb, i) => { - data.set(rgb, i * 4); - data[i * 4 + 3] = 255; - }); - return { width, height, data }; -} - -describe("extractMask", () => { - const pixels: [number, number, number][] = [ - [255, 0, 0], [230, 20, 10], [200, 0, 0], [255, 128, 0], [255, 100, 0], [128, 0, 255], - [120, 10, 250], [255, 255, 255], [0, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 128], - ]; - const img = image(6, 2, pixels); - - it("finds red that dominates: R − max(G, B) at least the threshold", () => { - expect(Array.from(extractMask(img, [255, 0, 0]))).toEqual([1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - // A lower threshold admits orange too: 255 − 128 = 127 is red that dominates by 100. - expect(Array.from(extractMask(img, [255, 0, 0], 100))).toEqual([1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]); - }); -}); diff --git a/web/test/unit/ai-annotate-plane.test.ts b/web/test/unit/ai-annotate-plane.test.ts deleted file mode 100644 index d4d962b4..00000000 --- a/web/test/unit/ai-annotate-plane.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -// From a planned plane to the image a model is sent (src/ai-annotate/plane.ts -// readPlane) against cellmap_flow/ai_annotate/pipeline.py: the raw read -// through a fake Inference whose readRaw records the box asked and answers a -// ramp, so the checks are where the read lands (the same physical area as the -// write box, with the level's origin taken as voxel 0's centre), how the -// plane is laid out (as the panel draws it) and the zeros past the level. The -// parity fixture checks the resampling, the mask put on the write box and the -// ids against the Python's own numbers, and the route tests -// (backend-ai-annotate.test.ts) run a plane all the way to labels. -import { describe, expect, it } from "vitest"; - -import type { DatasetInfo } from "../../src/contracts"; -import { planPlane } from "../../src/ai-annotate/geometry"; -import { readPlane } from "../../src/ai-annotate/plane"; -import type { DepthAxis } from "../../src/ai-annotate/types"; - -/** A dataset of one level of `shape` voxels at 4 nm whose voxel 0's centre is at `origin`. */ -function dataset(shape: number[], origin = [2, 2, 2], dtype = "uint8"): DatasetInfo { - return { - url: "https://example.org/raw.zarr", dtype, channels: 1, - axes: [{ name: "z", unit: "nanometer" }, { name: "y", unit: "nanometer" }, { name: "x", unit: "nanometer" }], - levels: [{ shape, voxel: [4, 4, 4], origin }], - }; -} - -/** An 8 nm annotation volume over a 4 nm raw, voxel 0's centre at `offset`. */ -function volume(offset = [4, 4, 4], output = [8, 8, 8], input = [4, 4, 4]) { - return { output_voxel_size: output, input_voxel_size: input, dataset_offset_nm: offset }; -} - -/** The raw voxel (z, y, x) holds this, never 0, exact in float32. */ -const rawValue = (z: number, y: number, x: number) => ((z * 7 + y) * 300 + x) % 60000 + 1; - -/** An Inference's readRaw over a level of `shape` whose voxel (z, y, x) is - * rawValue(z, y, x), recording every box asked and refusing one outside the - * level, as the engine's reader would. */ -function fakeInfer(shape: number[]) { - const reads: { level: number; lo: number[]; hi: number[] }[] = []; - return { - reads, - async readRaw(level: number, lo: number[], hi: number[]): Promise { - reads.push({ level, lo: [...lo], hi: [...hi] }); - for (let a = 0; a < 3; a++) { - if (lo[a] < 0 || hi[a] > shape[a] || hi[a] <= lo[a]) throw new Error(`a box ${lo.join(",")}..${hi.join(",")} is not within the level ${shape.join("×")}`); - } - const out = new Uint16Array((hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2])); - let k = 0; - for (let z = lo[0]; z < hi[0]; z++) for (let y = lo[1]; y < hi[1]; y++) for (let x = lo[2]; x < hi[2]; x++) out[k++] = rawValue(z, y, x); - return out.buffer; - }, - }; -} - -const LEVEL = [100, 200, 300]; // 4 nm voxels: 400 × 800 × 1200 nm -const VOLUME_SHAPE = [50, 100, 150]; // 8 nm voxels over the same extent - -describe("readPlane", () => { - for (const [origin, offset] of [[[2, 2, 2], [4, 4, 4]], [[102, 102, 102], [104, 104, 104]]]) { - it(`reads the raw over the write box's physical area (level origin ${origin.join(",")}, volume offset ${offset.join(",")})`, async () => { - // The level's corner and the volume's corner coincide (both 0, or both - // 100): a plan's write box in 8 nm voxels and its raw read in 4 nm - // voxels must cover the same nm. - const levelCorner = origin[0] - 2, volumeCorner = offset[0] - 4; - const data = dataset(LEVEL, origin); - for (const depthAxis of [0, 1, 2] as DepthAxis[]) { - const point = [volumeCorner + 201, volumeCorner + 403, volumeCorner + 606]; // in volume voxel (25, 50, 75) - const plan = planPlane(volume(offset), VOLUME_SHAPE, point, depthAxis, 16); - const infer = fakeInfer(LEVEL); - const plane = await readPlane(infer, data, volume(offset), plan); - expect(infer.reads.length).toBe(1); - const { level, lo, hi } = infer.reads[0]; - expect(level).toBe(0); - for (let a = 0; a < 3; a++) { - if (a === depthAxis) { - // The one raw voxel that holds the point. - expect(hi[a] - lo[a]).toBe(1); - expect(levelCorner + lo[a] * 4).toBeLessThanOrEqual(point[a]); - expect(levelCorner + hi[a] * 4).toBeGreaterThan(point[a]); - } else { - expect(levelCorner + lo[a] * 4, `axis ${a} start`).toBe(volumeCorner + plan.write_lo[a] * 8); - expect(levelCorner + hi[a] * 4, `axis ${a} end`).toBe(volumeCorner + plan.write_hi[a] * 8); - } - } - expect([plane.rows, plane.cols]).toEqual(plan.image_shape); - expect(plane.data).toBeInstanceOf(Float32Array); - } - }); - } - - it("lays the plane out as the panel draws it: XY with x leftwards, XZ and YZ transposed", async () => { - const data = dataset(LEVEL, [2, 2, 2], "uint16"); // the fake answers uint16 values - const point = [201, 403, 606]; - // XY (depth z): rows y, cols x running leftwards. - let infer = fakeInfer(LEVEL); - let plane = await readPlane(infer, data, volume(), planPlane(volume(), VOLUME_SHAPE, point, 0, 16)); - let { lo, hi } = infer.reads[0]; - expect([plane.rows, plane.cols]).toEqual([32, 32]); - for (const [r, c] of [[0, 0], [5, 31], [31, 7]]) expect(plane.data[r * 32 + c]).toBe(rawValue(lo[0], lo[1] + r, hi[2] - 1 - c)); - // XZ (depth y): rows x, cols z. - infer = fakeInfer(LEVEL); - plane = await readPlane(infer, data, volume(), planPlane(volume(), VOLUME_SHAPE, point, 1, 16)); - ({ lo } = infer.reads[0]); - for (const [r, c] of [[0, 0], [5, 31], [31, 7]]) expect(plane.data[r * 32 + c]).toBe(rawValue(lo[0] + c, lo[1], lo[2] + r)); - // YZ (depth x): rows y, cols z. - infer = fakeInfer(LEVEL); - plane = await readPlane(infer, data, volume(), planPlane(volume(), VOLUME_SHAPE, point, 2, 16)); - ({ lo } = infer.reads[0]); - for (const [r, c] of [[0, 0], [5, 31], [31, 7]]) expect(plane.data[r * 32 + c]).toBe(rawValue(lo[0] + c, lo[1] + r, lo[2])); - }); - - it("clips the read to the level and leaves zeros past it", async () => { - // The volume is padded past the level (to whole chunks): a plane near - // the far x edge reads to the level's end and is zero beyond, on the - // left of the XY image where x runs leftwards. - const padded = [50, 100, 160]; // 1280 nm in x over a level of 1200 - const point = [201, 403, 1196]; // x voxel 149 - const plan = planPlane(volume(), padded, point, 0, 16); - expect(plan.write_hi[2]).toBe(157); - const infer = fakeInfer(LEVEL); - const plane = await readPlane(infer, dataset(LEVEL, [2, 2, 2], "uint16"), volume(), plan); - expect(infer.reads[0].hi[2]).toBe(300); - expect(infer.reads[0].lo[2]).toBe(141 * 2); - expect([plane.rows, plane.cols]).toEqual([32, 32]); - // 314 − 300 = 14 raw columns past the level, mirrored to the left. - for (let c = 0; c < 14; c++) expect(plane.data[5 * 32 + c], `col ${c}`).toBe(0); - expect(plane.data[5 * 32 + 14]).toBe(rawValue(infer.reads[0].lo[0], infer.reads[0].lo[1] + 5, 299)); - }); -}); diff --git a/web/test/unit/ai-annotate-python.test.ts b/web/test/unit/ai-annotate-python.test.ts new file mode 100644 index 00000000..60a235c8 --- /dev/null +++ b/web/test/unit/ai-annotate-python.test.ts @@ -0,0 +1,164 @@ +// The AI annotation's calls into cellmap_flow.portable (portable/ +// ai_annotate_calls.py), here in node's Pyodide (test/unit/pyodide.ts): that +// each answers through the browser's wrappers (plane.ts, mask.ts, the job's +// accept), and what is the browser's own around them: where the raw read +// lands (a level's origin is voxel 0's centre, so the read covers the write +// box's physical area) and the zeros past the level. The maths is tested in +// pytest on the Python itself (tests/portable/test_ai_annotate.py); the route +// tests (backend-ai-annotate.test.ts) run a plane all the way to labels. +// Without Pyodide (no network for its wheels) these skip. +import { describe, expect, it, vi } from "vitest"; + +import { modelMask, stageMask } from "../../src/ai-annotate/mask"; +import { AI_PYTHON, planPlane, readImage } from "../../src/ai-annotate/plane"; +import type { DepthAxis, RgbaImage } from "../../src/ai-annotate/types"; +import type { DatasetInfo } from "../../src/contracts"; +import { portablePython } from "./pyodide"; + +// Pyodide, numpy, Pillow (for the resizes) and scipy (for the mask's objects) +// load once for the file. +vi.setConfig({ testTimeout: 120_000 }); +const python = await portablePython(); + +/** A dataset of one uint8 level of `shape` voxels at 4 nm whose voxel 0's centre is at `origin`. */ +function dataset(shape: number[], origin = [2, 2, 2]): DatasetInfo { + return { + url: "https://example.org/raw.zarr", dtype: "uint8", channels: 1, + axes: [{ name: "z", unit: "nanometer" }, { name: "y", unit: "nanometer" }, { name: "x", unit: "nanometer" }], + levels: [{ shape, voxel: [4, 4, 4], origin }], + }; +} + +/** An 8 nm annotation volume of `shape` voxels over a 4 nm raw, voxel 0's centre at `offset`. */ +function volume(shape: number[], offset = [4, 4, 4]) { + return { output_voxel_size: [8, 8, 8], input_voxel_size: [4, 4, 4], dataset_offset_nm: offset, dataset_shape_voxels: shape }; +} + +/** The raw voxel (z, y, x) holds this, never 0. */ +const rawValue = (z: number, y: number, x: number) => ((z * 7 + y) * 3 + x) % 250 + 1; + +/** An Inference with the portable Python and a readRaw over a level of + * `shape` whose voxel (z, y, x) is rawValue(z, y, x), recording every box + * asked and refusing one outside the level, as the engine's reader would. */ +function fakeInfer(shape: number[]) { + const reads: { level: number; lo: number[]; hi: number[] }[] = []; + return { + reads, + python: python!, + async readRaw(level: number, lo: number[], hi: number[]): Promise { + reads.push({ level, lo: [...lo], hi: [...hi] }); + for (let a = 0; a < 3; a++) { + if (lo[a] < 0 || hi[a] > shape[a] || hi[a] <= lo[a]) throw new Error(`a box ${lo.join(",")}..${hi.join(",")} is not within the level ${shape.join("×")}`); + } + const out = new Uint8Array((hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2])); + let k = 0; + for (let z = lo[0]; z < hi[0]; z++) for (let y = lo[1]; y < hi[1]; y++) for (let x = lo[2]; x < hi[2]; x++) out[k++] = rawValue(z, y, x); + return out.buffer; + }, + }; +} + +const LEVEL = [100, 200, 300]; // 4 nm voxels: 400 × 800 × 1200 nm +const VOLUME_SHAPE = [50, 100, 150]; // 8 nm voxels over the same extent + +/** A grey image of `rows` × `cols` with the pixels `painted` says painted `rgb`, as a model might answer. */ +function reply(rows: number, cols: number, painted: (r: number, c: number) => boolean, rgb = [255, 0, 0]): RgbaImage { + const data = new Uint8ClampedArray(rows * cols * 4); + for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) { + const j = (r * cols + c) * 4; + const [red, green, blue] = painted(r, c) ? rgb : [40, 40, 40]; + data.set([red, green, blue, 255], j); + } + return { width: cols, height: rows, data }; +} + +describe.skipIf(!python)("the AI annotation's portable Python", () => { + for (const [origin, offset] of [[[2, 2, 2], [4, 4, 4]], [[102, 102, 102], [104, 104, 104]]]) { + it(`plans a plane whose raw read covers its write box (level origin ${origin.join(",")}, volume offset ${offset.join(",")})`, async () => { + // The level's corner and the volume's corner coincide (both 0, or both + // 100): a write box in 8 nm voxels and its raw read in 4 nm voxels + // must cover the same nm. + const levelCorner = origin[0] - 2, volumeCorner = offset[0] - 4; + for (const depthAxis of [0, 1, 2] as DepthAxis[]) { + const point = [volumeCorner + 201, volumeCorner + 403, volumeCorner + 606]; // in volume voxel (25, 50, 75) + const infer = fakeInfer(LEVEL); + const planned = await planPlane(infer, dataset(LEVEL, origin), volume(VOLUME_SHAPE, offset), point, depthAxis, 16); + const image = await readImage(infer, dataset(LEVEL, origin), planned); + const { plan } = planned; + const { lo, hi } = infer.reads[0]; + for (let a = 0; a < 3; a++) { + if (a === depthAxis) { + // The one raw voxel that holds the point. + expect(hi[a] - lo[a]).toBe(1); + expect(levelCorner + lo[a] * 4).toBeLessThanOrEqual(point[a]); + expect(levelCorner + hi[a] * 4).toBeGreaterThan(point[a]); + } else { + expect(levelCorner + lo[a] * 4, `axis ${a} start`).toBe(volumeCorner + plan.write_lo[a] * 8); + expect(levelCorner + hi[a] * 4, `axis ${a} end`).toBe(volumeCorner + plan.write_hi[a] * 8); + } + } + expect([image.height, image.width]).toEqual(plan.image_shape); + expect(planned.write_shape).toEqual([16, 16]); + } + }); + } + + it("sends the plane as the panel draws it, stops it at the data's edge, and refuses a point outside the volume", async () => { + // The volume is padded past the level (to whole chunks): a plane near the + // far x edge stops where the data does (the level's extent, rawExtentNm), + // 9 voxels from 141. In the XY image x runs leftwards; a uint8 plane on + // the image's own grid is sent as it is, so the image is the raw's values. + const infer = fakeInfer(LEVEL); + const planned = await planPlane(infer, dataset(LEVEL), volume([50, 100, 160]), [201, 403, 1196], 0, 16); + expect([planned.plan.write_lo[2], planned.plan.write_hi[2]]).toEqual([141, 150]); + const image = await readImage(infer, dataset(LEVEL), planned); + const { lo, hi } = infer.reads[0]; + expect([lo[2], hi[2]]).toEqual([282, 300]); + expect([image.height, image.width]).toEqual([32, 18]); + const grey = (r: number, c: number) => image.data[(r * image.width + c) * 4]; + for (const c of [0, 9, 17]) expect(grey(5, c), `col ${c}`).toBe(rawValue(lo[0], lo[1] + 5, 299 - c)); + expect(image.data[3]).toBe(255); + await expect(planPlane(infer, dataset(LEVEL), volume(VOLUME_SHAPE), [-1, 403, 606], 0, 16)).rejects.toMatchObject({ category: "refused", httpStatus: 400 }); + }); + + it("takes the mask out of a reply at twice the size sent, lined up with the plane", async () => { + // The plane is 32 × 24, sent padded to 32 × 32; the model answered 64 × + // 64 with the plane's left half red (in its own pixels, the left 24). + const answer = reply(64, 64, (_r, c) => c < 24); + const { mask, model_image } = await modelMask(python!, answer, { width: 32, height: 32 }, { width: 24, height: 32 }, [255, 0, 0]); + expect([model_image.width, model_image.height, mask.length]).toEqual([24, 32, 24 * 32]); + expect(mask[10 * 24 + 5]).toBe(1); + expect(mask[10 * 24 + 20]).toBe(0); + }); + + it("stages the mask's objects on the write box with the review's overlay, and accepts them into the box", async () => { + const infer = fakeInfer(LEVEL); + const planned = await planPlane(infer, dataset(LEVEL), volume(VOLUME_SHAPE), [201, 403, 606], 0, 16); + const image = await readImage(infer, dataset(LEVEL), planned); + // Two blocks of the 32 × 32 image: two objects of the 16 × 16 write box. + const mask = Uint8Array.from({ length: 32 * 32 }, (_, i) => (Math.floor(i / 32) < 8 && i % 32 < 8) || (Math.floor(i / 32) >= 20 && i % 32 >= 20) ? 1 : 0); + const staged = await stageMask(python!, planned.plan, mask, image, [255, 0, 0]); + expect(staged.objects.length).toBe(16 * 16); + expect(new Set(staged.objects)).toEqual(new Set([0, 1, 2])); + expect(staged.maskFraction).toBeCloseTo((16 + 36) / 256); + expect(staged.overlay.data[0]).toBeGreaterThan(image.data[0]); + + // In the write box (z, y, x), x mirrored back: the first block is rows + // 0..3, x 12..15, the second rows 10..15, x 0..5. Accept into a uint8 box + // holding 7 under the first: it keeps 7, the second gets the lowest free + // id, the rest is background. + const existing = new Uint8Array(256); + existing[12] = 7; + const args = { depth_axis: 0, shape: [1, 16, 16], dtype: "uint8", instance_target: false, overwrite: false, beside: [false, false] }; + const answer = await python!<{ value: Record; arrays: ArrayBuffer[] }>(`${AI_PYTHON}:accept`, args, [staged.objects.slice().buffer, existing.slice().buffer]); + expect(answer.value).toEqual({ n_foreground: 16 + 36 - 1, n_background: 256 - 16 - 36, n_overwritten: 0 }); + const after = new Uint8Array(answer.arrays[0]); + expect([after[12], after[13], after[15 * 16], after[8 * 16]]).toEqual([7, 7, 2, 1]); + + // Every uint8 id is in use: an object on an unpainted voxel is refused, in the Python's words. + const full = Uint8Array.from({ length: 256 }, (_, i) => (i < 254 ? i + 2 : 0)); + const lastVoxel = Int32Array.from({ length: 256 }, (_, i) => (i === 255 ? 1 : 0)); + const refused = await python!<{ refused?: string }>(`${AI_PYTHON}:accept`, args, [lastVoxel.buffer, full.buffer]); + expect(refused.refused).toMatch(/do not fit in the volume's uint8 labels/); + }); +}); diff --git a/web/test/unit/backend-ai-annotate.test.ts b/web/test/unit/backend-ai-annotate.test.ts index 315720b6..52afa64c 100644 --- a/web/test/unit/backend-ai-annotate.test.ts +++ b/web/test/unit/backend-ai-annotate.test.ts @@ -5,8 +5,10 @@ // routes' refusals, their order and their statuses; and what each does to the // paint store (the mask written, the undo, the audit log), the viewer (the // key bound, the layer refreshed) and the stores (credentials, the day's -// count). -import { describe, expect, it } from "vitest"; +// count). A run plans, reads, masks and accepts its plane through the +// portable Python (here in node's Pyodide, test/unit/pyodide.ts), so the +// cases that run one skip without it. +import { describe, expect, it, vi } from "vitest"; import { readLog } from "../../src/ai-annotate/audit"; import { CREDENTIALS_KEY, createCredentialStore } from "../../src/ai-annotate/credentials"; @@ -29,6 +31,11 @@ import { readVolume } from "../../src/finetune/volume"; import { type KeyValueStore, MemoryKeyValueStore } from "../../src/models/listing-cache"; import { paintLayerName } from "../../src/viewer/layers"; import { entry, type FakeContext, fakeContext, NAME_54, NAME_56, request } from "./backend-fakes"; +import { portablePython } from "./pyodide"; + +// Pyodide, numpy, Pillow and scipy load once for the file, a few seconds. +vi.setConfig({ testTimeout: 120_000 }); +const python = await portablePython(); function body(res: ApiResponse): any { if (!("json" in res)) throw new Error(`not a JSON answer: ${JSON.stringify(res)}`); @@ -305,7 +312,7 @@ describe("POST /api/finetune/ai-annotate/settings", () => { }); }); -describe("POST /api/finetune/ai-annotate/run: the refusals, in _begin_run's order", () => { +describe.skipIf(!python)("POST /api/finetune/ai-annotate/run: the refusals, in _begin_run's order", () => { it("wants settings first, then a volume (needs_volume), then the acknowledgement, then the credentials", async () => { const ctx = fakeContext(); ctx.viewer.positionNm = POINT; @@ -384,7 +391,7 @@ describe("POST /api/finetune/ai-annotate/run: the refusals, in _begin_run's orde }); }); -describe("a run on another dataset than the volume's", () => { +describe.skipIf(!python)("a run on another dataset than the volume's", () => { it("is refused before anything is sent or counted: the plane would be read from one dataset and written on another's grid", async () => { const { ctx, h, id } = await ready(); ctx.viewer.positionNm = POINT; @@ -398,7 +405,7 @@ describe("a run on another dataset than the volume's", () => { }); }); -describe("a run to ready", () => { +describe.skipIf(!python)("a run to ready", () => { it("reads the plane around the point, asks the model, and stages the mask with its preview; the status's fields are set as it goes", async () => { const { ctx, h, id } = await ready(); expect((await status(h))).toMatchObject({ success: true, annotate_id: null, status: "idle", error: null, preview: null, needs_volume: false }); @@ -475,7 +482,7 @@ describe("a run to ready", () => { }); -describe("Shift+G", () => { +describe.skipIf(!python)("Shift+G", () => { it("without a volume says so with needs_volume, for the page to offer one", async () => { const ctx = fakeContext(); const h = harness(ctx); @@ -510,7 +517,7 @@ describe("Shift+G", () => { }); }); -describe("POST /api/finetune/ai-annotate/resend", () => { +describe.skipIf(!python)("POST /api/finetune/ai-annotate/resend", () => { it("sends the same plane again with the edited prompt under the same id, and replaces the preview", async () => { const gemini = new TestProvider(); const { ctx, h, id } = await ready({ deps: { providers: providers(gemini) }, settings: { provider: "gemini", model: "gemini-3-pro-image", destination: gemini.info.destination } }); @@ -540,7 +547,7 @@ describe("POST /api/finetune/ai-annotate/resend", () => { }); -describe("POST /api/finetune/ai-annotate/accept", () => { +describe.skipIf(!python)("POST /api/finetune/ai-annotate/accept", () => { it("writes the mask's objects with ids of their own and background elsewhere in the plane, refreshes the layer, and leaves nothing to review", async () => { const { ctx, h, id, volume } = await ready(); const annotateId = await runToEnd(h); @@ -625,7 +632,7 @@ describe("POST /api/finetune/ai-annotate/accept", () => { }); -describe("POST /api/finetune/view-labels/undo", () => { +describe.skipIf(!python)("POST /api/finetune/view-labels/undo", () => { it("restores the box as it was, keeping a stroke painted since, and says when nothing is left", async () => { const { ctx, h, volume } = await ready(); const annotateId = await runToEnd(h); @@ -649,7 +656,7 @@ describe("POST /api/finetune/view-labels/undo", () => { }); -describe("POST /api/finetune/ai-annotate/reject", () => { +describe.skipIf(!python)("POST /api/finetune/ai-annotate/reject", () => { it("throws the staged mask away, writing nothing, and takes a failed job too", async () => { const { ctx, h, id } = await ready(); const annotateId = await runToEnd(h); @@ -687,7 +694,7 @@ describe("the daily limit", () => { expect(inMemory.callsToday()).toBe(1); }); - it("is applied to a run (429) and shown by the config", async () => { + it.skipIf(!python)("is applied to a run (429) and shown by the config", async () => { const { h } = await ready({ deps: { dailyCallLimit: 1 } }); const annotateId = await runToEnd(h); expect(body(await h.handle(request("GET", `${BASE}/config`)))).toMatchObject({ daily_call_limit: 1, calls_today: 1 }); diff --git a/web/test/unit/backend-finetune.test.ts b/web/test/unit/backend-finetune.test.ts index 93a390d1..98aa000d 100644 --- a/web/test/unit/backend-finetune.test.ts +++ b/web/test/unit/backend-finetune.test.ts @@ -18,6 +18,12 @@ import { entryFor, parseManifest } from "../../src/models/manifest"; import { DATASET, entry, type FakeContext, fakeContext, NAME_54, NAME_56, request } from "./backend-fakes"; import { rawManifest } from "./models-helpers"; import { readMetadata } from "./onnx-metadata"; +import { portablePython } from "./pyodide"; + +// A volume's plan and a region's rules are the portable Python's, here in +// node's Pyodide (pyodide.ts), loaded once for the file before the tests; +// without it (no network for numpy's wheel) they skip. +const python = await portablePython(); function body(res: ApiResponse): any { if (!("json" in res)) throw new Error(`not a JSON answer: ${JSON.stringify(res)}`); @@ -111,7 +117,7 @@ function fakeRun(baton?: Baton): { run: RunFinetune; calls: FinetuneRun[]; resul function harness(ctx: FakeContext, deps: Partial = {}) { const router = new Router(); let volumes = 0, second = 0; - const plan: FinetuneDeps["plan"] = (dataset, e, chain, options = {}) => planVolume(dataset, e, chain, { ...options, id: `vol-${String(++volumes).padStart(8, "0")}-20261015-120000` }); + const plan: FinetuneDeps["plan"] = (infer, dataset, e, chain, options = {}) => planVolume(infer, dataset, e, chain, { ...options, id: `vol-${String(++volumes).padStart(8, "0")}-20261015-120000` }); const jobs = finetuneRoutes(router, { plan, now: () => new Date(2026, 9, 15, 12, 0, second++), ...deps }); return { handle: (req: ApiRequest) => router.handle(req, ctx), jobs }; } @@ -191,7 +197,7 @@ const GRAPH_3D = parseTrainGraph({ ], }); -describe("GET /api/finetune/models", () => { +describe.skipIf(!python)("GET /api/finetune/models", () => { it("offers a 3D model's whole network in its files' block sides with what a step costs, one block a step, and trains the file of the block", async () => { // setup_53 served from its 124³ fp32 file, its manifest's full block as on the Hub since 2026-10-17. const file = "setup_53_mito_affinities_16nm_in124_fp32.onnx"; @@ -236,7 +242,7 @@ describe("GET /api/finetune/models", () => { }); }); -describe("the existing volumes", () => { +describe.skipIf(!python)("the existing volumes", () => { it("resumes a volume as the session's, in place, with its painted chunks counted and its record as metadata", async () => { const { ctx, h, id } = await ready(); await ctx.session.update({ paintVolume: null }); @@ -266,7 +272,7 @@ describe("the existing volumes", () => { }); }); -describe("POST /api/finetune/submit", () => { +describe.skipIf(!python)("POST /api/finetune/submit", () => { it("refuses, with needs_volume, when the session has no annotation volume, and without it when nothing is painted", async () => { const ctx = fakeContext(); await serve(ctx, entry("setup_54")); @@ -281,7 +287,7 @@ describe("POST /api/finetune/submit", () => { }); }); -describe("a job's life", () => { +describe.skipIf(!python)("a job's life", () => { it("runs the model on the volume with the form's options, and reports as job-card.js reads it once served", async () => { const { ctx, h, id, calls } = await ready(); const base = ctx.infer.served().find((s) => s.entry.name === NAME_54)!.entry; @@ -359,7 +365,7 @@ describe("a job's life", () => { }); }); -describe("GET /api/finetune/job/:id/logs/stream", () => { +describe.skipIf(!python)("GET /api/finetune/job/:id/logs/stream", () => { it("sends the log so far with byte-offset ids, then each line as it is logged, and done when the job ends", async () => { const baton = new Baton(); const { h, id } = await ready({ baton }); @@ -390,7 +396,7 @@ describe("GET /api/finetune/job/:id/logs/stream", () => { }); }); -describe("cancel, stop early, restart", () => { +describe.skipIf(!python)("cancel, stop early, restart", () => { it("cancels a running job at once; the run stops at its next step and nothing of it is kept", async () => { const baton = new Baton(); const { ctx, h, results } = await ready({ baton }); @@ -465,7 +471,7 @@ describe("cancel, stop early, restart", () => { }); }); -describe("Shift+R over the viewer", () => { +describe.skipIf(!python)("Shift+R over the viewer", () => { it("is bound once the tab asks for the jobs, and tells the page, whose job-monitor.js restarts the job; the job itself is left to it", async () => { const page = new EventTarget(); const heard: string[] = []; @@ -487,7 +493,7 @@ describe("Shift+R over the viewer", () => { }); }); -describe("downloads and deletion", () => { +describe.skipIf(!python)("downloads and deletion", () => { it("downloads the volume as a zip of its zarr, every file under .zarr/", async () => { const { h, id } = await ready(); const res = await h.handle(request("GET", `/api/finetune/volume/${id}/download`)); @@ -537,7 +543,7 @@ describe("downloads and deletion", () => { }); }); -describe("in the backend", () => { +describe.skipIf(!python)("in the backend", () => { it("answers the finetune routes", async () => { const ctx = fakeContext(); await serve(ctx, entry("setup_54")); diff --git a/web/test/unit/backend-good-regions.test.ts b/web/test/unit/backend-good-regions.test.ts index 5102fb83..62aa5fdb 100644 --- a/web/test/unit/backend-good-regions.test.ts +++ b/web/test/unit/backend-good-regions.test.ts @@ -13,6 +13,12 @@ import { createVolume } from "../../src/finetune/volume"; import { defaultChain } from "../../src/state"; import { GOOD_REGIONS_LAYER } from "../../src/viewer/layers"; import { DATASET, type FakeContext, fakeContext, request } from "./backend-fakes"; +import { portablePython } from "./pyodide"; + +// A volume's plan and a region's rules are the portable Python's, here in +// node's Pyodide (pyodide.ts), loaded once for the file before the tests; +// without it (no network for numpy's wheel) they skip. +const python = await portablePython(); function body(res: ApiResponse): any { if (!("json" in res)) throw new Error(`not a JSON answer: ${JSON.stringify(res)}`); @@ -42,7 +48,7 @@ function drawn(ctx: FakeContext): unknown[][] { return layer.annotations.map((a) => [a.pointA, a.pointB, a.id, a.description]); } -describe("POST /api/finetune/good-regions/mark-view", () => { +describe.skipIf(!python)("POST /api/finetune/good-regions/mark-view", () => { it("marks one output block centred where the viewer looks, keeps it with the volume, and draws it", async () => { const { ctx, handle } = await harness(); ctx.viewer.positionNm = [16, 32, 32]; @@ -63,7 +69,7 @@ describe("POST /api/finetune/good-regions/mark-view", () => { }); }); -describe("POST /api/finetune/good-regions/delete", () => { +describe.skipIf(!python)("POST /api/finetune/good-regions/delete", () => { it("deletes one region by id, every region without one, and answers 404 for an id the session has not", async () => { const { ctx, handle } = await harness(); ctx.viewer.positionNm = [16, 32, 32]; diff --git a/web/test/unit/backend-view-labels.test.ts b/web/test/unit/backend-view-labels.test.ts index be7b5a7b..ec911c6f 100644 --- a/web/test/unit/backend-view-labels.test.ts +++ b/web/test/unit/backend-view-labels.test.ts @@ -3,10 +3,13 @@ // routes for a served model, the viewer's position set, the model's output // a fake's (FakeInference.outputAt), and the answers, the paint store, the // viewer's refreshes and the undo stack checked against the Python routes' -// behaviour (view_labels.py). The numerics underneath (the probability, the -// objects, the relabelling, the grids) are parity-tested against the Python -// in test/parity/label-it.test.ts; these tests are about the routes. -import { describe, expect, it } from "vitest"; +// behaviour (view_labels.py). A seed's maths and every tool's box, ids, +// writes and undo are the portable Python (label_it.py, fill.py), run here +// in node's Pyodide (test/unit/pyodide.ts) and tested by pytest; the mutex +// watershed is parity-tested against the Python in +// test/parity/label-it.test.ts; these tests are about the routes. Without +// Pyodide (no network for its wheels) every test but the sources' skips. +import { describe, expect, it, vi } from "vitest"; import { readBox, writeBox } from "../../src/ai-annotate/fill"; import { Router } from "../../src/backend/router"; @@ -17,6 +20,12 @@ import type { RunFinetune } from "../../src/finetune/types"; import { readVolume } from "../../src/finetune/volume"; import { paintLayerName } from "../../src/viewer/layers"; import { entry, type FakeContext, fakeContext, NAME_54, NAME_56, request } from "./backend-fakes"; +import { portablePython } from "./pyodide"; + +// Pyodide and numpy load once for the file, scipy at the first seed, a few +// seconds each; each call after is quick. +vi.setConfig({ testTimeout: 120_000 }); +const python = await portablePython(); // eslint-disable-next-line @typescript-eslint/no-explicit-any function body(res: ApiResponse): any { @@ -110,7 +119,7 @@ describe("GET /api/finetune/view-labels/sources", () => { }); }); -describe("POST /api/finetune/view-labels/seed", () => { +describe.skipIf(!python)("POST /api/finetune/view-labels/seed", () => { it("fills the patch from the prediction: an id per object counting up on a uint16 volume, background 1, the layer re-read", async () => { const { ctx, h, volume, id } = await ready(); const res = body(await h.handle(request("POST", `${BASE}/seed`, {}))); @@ -160,6 +169,7 @@ describe("POST /api/finetune/view-labels/seed", () => { [{ min_size: -2 }, 400], [{ method: "watershed" }, 400], [{ connectivity: 4 }, 400], + [{ connectivity: null }, 400], // as the server reads it: given, and not 1, 2 or 3 [{ box_voxels: [1, 2] }, 400], [{ model: "nope" }, 409], [{ method: "mutex_watershed" }, 400], // setup_56 offers components only @@ -197,7 +207,7 @@ describe("POST /api/finetune/view-labels/seed", () => { }); }); -describe("POST /api/finetune/view-labels/background", () => { +describe.skipIf(!python)("POST /api/finetune/view-labels/background", () => { it("labels the unpainted voxels of the patch background, keeps the painted ones, and says when nothing is left", async () => { const { ctx, h, volume } = await ready(); await paintVoxel(ctx, volume, [3, 3, 3], 5); @@ -214,7 +224,7 @@ describe("POST /api/finetune/view-labels/background", () => { }); }); -describe("POST /api/finetune/view-labels/split", () => { +describe.skipIf(!python)("POST /api/finetune/view-labels/split", () => { it("gives the two sides of a wall their own ids, and merges a stroke's objects, per slice when asked", async () => { const { ctx, h, volume } = await ready(); const object = new Uint16Array(4 * 4 * 9).fill(5); @@ -236,11 +246,15 @@ describe("POST /api/finetune/view-labels/split", () => { }); }); -describe("POST /api/finetune/view-labels/undo", () => { +describe.skipIf(!python)("POST /api/finetune/view-labels/undo", () => { it("takes back the label actions last first, keeping a stroke painted since, until there is nothing left", async () => { const { ctx, h, volume } = await ready(); const background = body(await h.handle(request("POST", `${BASE}/background`, { box_voxels: 4 }))); expect([background.filled_background, background.can_undo]).toEqual([64, true]); + // With no dataset open the Python cannot run: refused, and the entry kept. + const closed = vi.spyOn(ctx.infer, "dataset").mockReturnValue(null); + expect((await h.handle(request("POST", `${BASE}/undo`, {}))).status).toBe(409); + closed.mockRestore(); const seeded = body(await h.handle(request("POST", `${BASE}/seed`, {}))); expect(seeded.filled_foreground + seeded.filled_background).toBe(60 ** 3 - 64); await paintVoxel(ctx, volume, [20, 20, 20], 9); // a stroke over the seed diff --git a/web/test/unit/finetune-full.test.ts b/web/test/unit/finetune-full.test.ts index 098a87ba..d56d91be 100644 --- a/web/test/unit/finetune-full.test.ts +++ b/web/test/unit/finetune-full.test.ts @@ -19,9 +19,8 @@ import { parseTrainGraph } from "../../src/finetune/full/graph"; import { fullTrainStep, supervisedLoss } from "../../src/finetune/full/loss"; import { onnxInitializerOffsets } from "../../src/finetune/full/run-full"; import { loadTf } from "../../src/finetune/full/tfjs"; -import { blockLabels, blockRaw, blockTargets, normalizeTile, paintedVoxels, planBlocks } from "../../src/finetune/full/tiles"; +import { blockLabels, blockRaw, normalizeTile, paintedVoxels, planBlocks } from "../../src/finetune/full/tiles"; import { batchLoss, mulberry32 } from "../../src/finetune/head-trainer"; -import { affinityOffsets, affinityTargets } from "../../src/finetune/targets"; import type { PaintedChunk } from "../../src/finetune/types"; const FIXTURES = resolve(import.meta.dirname, "../fixtures"); @@ -80,7 +79,7 @@ function trained(tf: Tf, bytes: Uint8Array, steps: number) { } describe("a 3D model's training blocks", () => { - it("hold the raw around the paint's cube in register with it, through flips of z and turns, and pair z neighbours", async () => { + it("hold the raw around the paint's cube in register with it, through flips of z and turns", async () => { // A 20³ volume in 4³ chunks, read for a network of 6³ in and 2³ out // (context 4: the input is the output cube grown by 2 each way). The // paint: a 2³ object at (8..9)³, its corner voxel another object (3); @@ -118,11 +117,6 @@ describe("a 3D model's training blocks", () => { const l = at(transformBlock(labels, [output, output, output], d).indexOf(3), output); expect(r.map((v) => v - halo)).toEqual(l); } - // The z affinity pairs each voxel of the lower slice with the one above: - // 3 over 2 is a boundary, 2 over 2 the same object. - const t = blockTargets(labels, [output, output, output], affinityTargets(affinityOffsets(["mito_aff_z", "mito_aff_y", "mito_aff_x"]), 3)); - const zPairs = [0, 1, 2, 3].map((p) => [t.mask[p * 3], t.target[p * 3]]); - expect(zPairs).toEqual([[1, 0], [1, 1], [1, 1], [1, 1]]); }); it("repeat the volume's normalized edge past it, as the model stage serves a block", async () => { diff --git a/web/test/unit/finetune-run.test.ts b/web/test/unit/finetune-run.test.ts index 4e80e7b2..30341f8b 100644 --- a/web/test/unit/finetune-run.test.ts +++ b/web/test/unit/finetune-run.test.ts @@ -16,16 +16,22 @@ import { describe, expect, it, vi } from "vitest"; import type { Chain, DatasetInfo, ModelEntry, PaintVolume } from "../../src/contracts"; import { createFeatureRunner, type FeatureRunner } from "../../src/finetune/features"; -import { regionAt } from "../../src/finetune/good-regions"; import { headOf } from "../../src/finetune/onnx-head"; import { createRunFinetune, type RunDeps } from "../../src/finetune/run"; import { DEFAULT_OPTIONS, type FinetuneRun, type HeadFinetuned, type PaintedChunk, type Sample, type Targets } from "../../src/finetune/types"; import { CHUNK_DIR, chunkKey } from "../../src/finetune/volume"; import { MemoryPaintStore } from "../../src/state/paint-store"; +import { portablePython, type PythonCall } from "./pyodide"; // The page's ORT entry replaced by the package's Node entry (see finetune-onnx.test.ts). vi.mock("onnxruntime-web/webgpu", () => import("onnxruntime-web")); +// The good regions' anchors are the portable Python's, here in node's +// Pyodide (pyodide.ts), loaded once for the file; without it (no network for +// numpy's wheel) the run with good regions skips. +const python = await portablePython(); +const callPython: PythonCall = python ?? (() => Promise.reject(new Error("Pyodide is not available in this test run"))); + const HEAD = new Uint8Array(readFileSync(resolve(import.meta.dirname, "../fixtures/head.onnx"))); const BASE = HEAD.buffer.slice(HEAD.byteOffset, HEAD.byteOffset + HEAD.byteLength) as ArrayBuffer; const CHUNK = [1, 6, 6], IN_DIMS = [1, 1, 1, 8, 8]; @@ -60,7 +66,7 @@ function block(index: number[]): Float32Array { } /** The binary target broadcast to both channels: object where the label is 2 or more, masked where painted. */ -function targets(c: PaintedChunk): Targets { +async function targets(c: PaintedChunk): Promise { const n = 36, target = new Float32Array(2 * n), mask = new Float32Array(2 * n); for (let v = 0; v < n; v++) for (let ch = 0; ch < 2; ch++) { target[ch * n + v] = c.labels[v] >= 2 ? 1 : 0; @@ -97,7 +103,7 @@ function deps(collectSamples: () => AsyncGenerator, disposed: { count: n } function run(over: Partial, paint: MemoryPaintStore, report: ReturnType): FinetuneRun { - return { entry: ENTRY, baseBytes: BASE, volume: VOLUME, chain: CHAIN, dataset: DATASET, infer: { readRaw: async () => new ArrayBuffer(0) }, paint, options: { ...DEFAULT_OPTIONS, epochs: 8, learningRate: 0.05, batchPixels: 16 }, report, ...over }; + return { entry: ENTRY, baseBytes: BASE, volume: VOLUME, chain: CHAIN, dataset: DATASET, infer: { readRaw: async () => new ArrayBuffer(0), python: callPython }, paint, options: { ...DEFAULT_OPTIONS, epochs: 8, learningRate: 0.05, batchPixels: 16 }, report, ...over }; } async function logitsOf(bytes: ArrayBuffer, input: Float32Array): Promise { @@ -184,12 +190,12 @@ function byIndex(chunks: PaintedChunk[]) { // 1×6×6 blocks. A good region centred on block (0, 1, 1), which nothing is // painted in, one on block (0, 0, 1), painted on 5 voxels, and one far // outside the volume. -const ON_UNPAINTED = regionAt([0, 68, 68], [8, 48, 48], 0, undefined, "aaaa0001"); -const ON_PAINTED = regionAt([0, 20, 68], [8, 48, 48], 1, undefined, "aaaa0002"); -const OUTSIDE = regionAt([0, 500, 500], [8, 48, 48], 2, undefined, "aaaa0003"); +const ON_UNPAINTED = { id: "aaaa0001", label: "good-1", offset_nm: [-4, 44, 44], shape_nm: [8, 48, 48] }; +const ON_PAINTED = { id: "aaaa0002", label: "good-2", offset_nm: [-4, -4, 44], shape_nm: [8, 48, 48] }; +const OUTSIDE = { id: "aaaa0003", label: "good-3", offset_nm: [-4, 476, 476], shape_nm: [8, 48, 48] }; describe("runFinetune's distillation", () => { - it("with good regions, holds their unpainted voxels instead of the form's scope, running the model on a region's unpainted block", async () => { + it.skipIf(!python)("with good regions, holds their unpainted voxels instead of the form's scope, running the model on a region's unpainted block", async () => { const { paint } = await painted(CHUNKS); const report = reporter(); const goodRegions = [ON_UNPAINTED, ON_PAINTED, OUTSIDE, { not: "a region" }]; diff --git a/web/test/unit/finetune-targets.test.ts b/web/test/unit/finetune-targets.test.ts new file mode 100644 index 00000000..22af908e --- /dev/null +++ b/web/test/unit/finetune-targets.test.ts @@ -0,0 +1,46 @@ +// The training targets through the browser's call (src/finetune/ +// target-call.ts): cellmap_flow/portable/targets.py in node's Pyodide +// (test/unit/pyodide.ts), as the engine's worker runs it, from a model +// entry as the trainers and the ranking ask for them. The maths itself is +// tested by pytest (tests/finetune/test_target_transforms.py). Without +// Pyodide (no network for numpy's wheel) these skip. +import { describe, expect, it, vi } from "vitest"; + +import { blockTargets } from "../../src/finetune/full/tiles"; +import { targetTransformFor } from "../../src/finetune/target-call"; +import { entry } from "./backend-fakes"; +import { portablePython } from "./pyodide"; + +// Pyodide and numpy load once for the file, a few seconds; each call after is quick. +vi.setConfig({ testTimeout: 120_000 }); +const python = await portablePython(); + +describe.skipIf(!python)("a chunk's targets, from the portable Python", () => { + const infer = { python: python! }; + + it("are an affinity model's along the offsets its channels name, and a binary model's on every channel", async () => { + // One slice of 2 × 3, an instance volume's ids: 2 2 3 over 1 0 2. + const chunk = { index: [0, 0, 0], shape: [1, 2, 3], labels: Uint16Array.from([2, 2, 3, 1, 0, 2]) }; + const aff = await targetTransformFor(entry("setup_54"), infer)(chunk); + // z pairs nothing in one slice; y pairs the rows where both are painted, + // 2 over 1 and 3 over 2, neither one object; x pairs 2|2 (one object) and 2|3. + expect(aff.channels).toBe(3); + expect(Array.from(aff.mask)).toEqual([0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0]); + expect(Array.from(aff.target)).toEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]); + + const nuc = await targetTransformFor(entry("setup_56", "nuc", ["nuc"], 128), infer)({ ...chunk, labels: Uint8Array.from(chunk.labels) }); + expect(nuc.channels).toBe(1); + expect(Array.from(nuc.target)).toEqual([1, 1, 1, 0, 0, 1]); + expect(Array.from(nuc.mask)).toEqual([1, 1, 1, 1, 0, 1]); + }); + + it("pair a cube's z neighbours, channels last, for the whole network's blocks", async () => { + // A 2³ object whose corner voxel is another object (3): the z affinity + // pairs each voxel of the lower slice with the one above, 3 over 2 a + // boundary, 2 over 2 the same object. + const labels = new Uint16Array(8).map((_, i) => (i ? 2 : 3)); + const t = await blockTargets(labels, [2, 2, 2], targetTransformFor(entry("setup_54"), infer)); + const zPairs = [0, 1, 2, 3].map((p) => [t.mask[p * 3], t.target[p * 3]]); + expect(zPairs).toEqual([[1, 0], [1, 1], [1, 1], [1, 1]]); + }); +}); diff --git a/web/test/unit/finetune-volume.test.ts b/web/test/unit/finetune-volume.test.ts index b7987439..b6c4bb14 100644 --- a/web/test/unit/finetune-volume.test.ts +++ b/web/test/unit/finetune-volume.test.ts @@ -1,16 +1,22 @@ -// An annotation volume (src/finetune/volume.ts) against -// cellmap_flow/finetune/session/volume.py: where planVolume lays a volume -// for a model over a dataset (numbers computed by hand from the geometry -// formulas, with the arithmetic beside them), the labels' dtype (common.py's -// annotation_dtype_for), and the round trip through a paint store, the -// painted chunks read back as the brush wrote them. -import { describe, expect, it } from "vitest"; +// An annotation volume (src/finetune/volume.ts): its plan answered by the +// portable Python (cellmap_flow/portable/volume.py, whose rules +// tests/portable/test_volume.py tests; here in node's Pyodide, +// test/unit/pyodide.ts, so without it that test skips) on the grid the +// engine serves the model's prediction on, and the round trip through a +// paint store, the painted chunks read back as the brush wrote them. +import { describe, expect, it, vi } from "vitest"; import type { Chain, DatasetInfo, ModelVariant, PaintVolume } from "../../src/contracts"; import { CHUNK_DIR, createVolume, paintedChunkIndices, planVolume, readPaintedChunk, readVolume } from "../../src/finetune/volume"; +import { chooseRawLevel, geometryOf, outputLevel } from "../../src/infer/geometry"; import { entryFor } from "../../src/models"; import { MemoryPaintStore } from "../../src/state/paint-store"; import { DATASET, entry, manifest, NAME_54 } from "./backend-fakes"; +import { portablePython } from "./pyodide"; + +// Pyodide and numpy load once for the file, a few seconds; each call after is quick. +vi.setConfig({ testTimeout: 120_000 }); +const python = await portablePython(); const chain: Chain = { input_norm: [{ name: "MinMaxNormalizer", min_value: 0, max_value: 255 }, { name: "LambdaNormalizer", expression: "2*x-1" }], @@ -18,81 +24,57 @@ const chain: Chain = { }; const NOW = new Date(2026, 9, 15, 13, 4, 5); // 2026-10-15 13:04:05 local -describe("planVolume (plan_volume)", () => { - it("lays setup_54's volume on the served prediction's grid, padded to whole chunks, with uint16 labels for its affinities", () => { - // setup_54's fp32 variant: 148³ voxels in, 60³ out, at 16 nm; the dataset - // one level, 1000³ voxels at 8 nm, origin 0. chooseRawLevel finds no - // level at 16 nm and no level coarser than 16 nm, so level 0 (8 nm) is - // read as if it were 16 nm. The ratio (output / input voxel size) is 1: - // output voxel = 8 × 1 = 8 nm - // level shape = floor(1000 × 16 / 16) = 1000 voxels - // origin = 0 - 8/2 + 8/2 = 0 (equal voxel sizes: the raw origin) - // padded shape = ceil(1000 / 60) × 60 = 17 × 60 = 1020 voxels - const volume = planVolume(DATASET, entry("setup_54"), chain, { id: "vol-abc12345-20261015-130405", now: NOW }); - expect(volume).toMatchObject({ - chunk_size: [60, 60, 60], - input_size: [148, 148, 148], - output_voxel_size: [8, 8, 8], - input_voxel_size: [8, 8, 8], - claimed_input_voxel_size: [16, 16, 16], - claimed_output_voxel_size: [16, 16, 16], - dataset_offset_nm: [0, 0, 0], - dataset_shape_voxels: [1020, 1020, 1020], - dtype: "uint16", - model_name: NAME_54, - dataset_path: DATASET.url, - input_norm: chain.input_norm, - created_at: NOW.toISOString(), - }); - // Copies: the chain the dashboard goes on editing is not the volume's. - expect(volume.input_norm).not.toBe(chain.input_norm); - }); +/** A volume as planVolume makes one, for the paint store's round trip. */ +const VOLUME: PaintVolume = { + id: "vol-rt", type: "annotation_volume", model_name: NAME_54, model_id: "test/setup_54", dataset_path: DATASET.url, + chunk_size: [60, 60, 60], input_size: [148, 148, 148], output_voxel_size: [8, 8, 8], input_voxel_size: [8, 8, 8], + claimed_output_voxel_size: [16, 16, 16], claimed_input_voxel_size: [16, 16, 16], dataset_offset_nm: [0, 0, 0], + dataset_shape_voxels: [1020, 1020, 1020], dtype: "uint16", input_norm: chain.input_norm, postprocess: chain.postprocess, + created_at: NOW.toISOString(), +}; - it("starts an anisotropic level's volume at its corner plus half an output voxel, a voxel centre as the OME translation is", () => { - // A model reading 8 nm and writing 16 nm (40³ voxels in, 8³ out), over a - // dataset whose one level is 16 × 8 × 8 nm, 101 × 1000 × 1000 voxels, - // origin (8, 4, 4) (voxel centres: the level's corner is 0). No level is - // at 8 nm and level 0 is already coarser along z, so level 0 is read as - // if it were 8 nm isotropic. The ratio is 2 on every axis: - // output voxel = (16, 8, 8) × 2 = (32, 16, 16) nm - // level shape = floor((101, 1000, 1000) × 8 / 16) = (50, 500, 500) - // origin = corner + half an output voxel = (8 - 8 + 16, 4 - 4 + 8, 4 - 4 + 8) = (16, 8, 8) - // padded shape = ceil((50, 500, 500) / 8) × 8 = (7 × 8, 63 × 8, 63 × 8) = (56, 504, 504) +describe("planVolume (plan_volume)", () => { + it.skipIf(!python)("is the Python's plan, on the grid the model is served on, with uint16 labels for an affinity model", async () => { + // A model reading 8 nm and writing 16 over an anisotropic level of 16 × + // 8 × 8 nm: the served level is the level's voxel times the ratio, from + // its corner, and the volume lies on it, one chunk per output block, + // over the level's extent padded to whole chunks. const variant: ModelVariant = { variant: "fp32", input_size: [40, 40, 40], output_size: [8, 8, 8], file: "coarse_fp32.onnx", file_mb: 1 }; const coarse = entryFor("coarse", { ...manifest("coarse", ["nuc"], 8), output_voxel_size_nm: [16, 16, 16], variants: [variant] }, variant); const dataset: DatasetInfo = { ...DATASET, levels: [{ shape: [101, 1000, 1000], voxel: [16, 8, 8], origin: [8, 4, 4] }] }; - const volume = planVolume(dataset, coarse, { input_norm: [], postprocess: [] }, { id: "vol-anis", now: NOW }); - expect(volume).toMatchObject({ - input_voxel_size: [16, 8, 8], - output_voxel_size: [32, 16, 16], - claimed_input_voxel_size: [8, 8, 8], - claimed_output_voxel_size: [16, 16, 16], - dataset_offset_nm: [16, 8, 8], - chunk_size: [8, 8, 8], - dataset_shape_voxels: [56, 504, 504], - dtype: "uint8", // a plain binary model - }); + const volume = await planVolume({ python: python! }, dataset, coarse, chain, { id: "vol-anis", now: NOW }); + const g = geometryOf(coarse), served = outputLevel(dataset.levels[chooseRawLevel(dataset.levels, g.input_voxel_size)], g); + expect(volume.output_voxel_size).toEqual(served.voxel); + expect(volume.dataset_offset_nm).toEqual(served.origin); + // Whole chunks over all of the served level (the far partial voxel too: the Python's to say). + expect(volume.dataset_shape_voxels.every((n, a) => n % 8 === 0 && n >= served.shape[a])).toBe(true); + expect(volume).toMatchObject({ chunk_size: [8, 8, 8], input_size: [40, 40, 40], dtype: "uint8", id: "vol-anis", created_at: NOW.toISOString() }); + // Copies: the chain the dashboard goes on editing is not the volume's. + expect(volume.input_norm).toEqual(chain.input_norm); + expect(volume.input_norm).not.toBe(chain.input_norm); + + expect((await planVolume({ python: python! }, DATASET, entry("setup_54"), chain)).dtype).toBe("uint16"); }); - it("refuses a dataset with more than one channel, as serve() does", () => { + it("refuses a dataset with more than one channel, as serve() does, before asking the Python", async () => { const rgb: DatasetInfo = { ...DATASET, channels: 3, axes: [{ name: "c", unit: "" }, ...DATASET.axes] }; - expect(() => planVolume(rgb, entry("setup_54"), chain)).toThrow(/channels/); + const noPython = { python: () => Promise.reject(new Error("not asked")) }; + await expect(planVolume(noPython, rgb, entry("setup_54"), chain)).rejects.toThrow(/channels/); }); }); describe("createVolume and readVolume on a paint store", () => { it("round-trips the volume through its files, the labels placed at voxel 0's centre", async () => { const store = new MemoryPaintStore(); - const volume = planVolume(DATASET, entry("setup_54"), chain, { id: "vol-rt", now: NOW }); - await createVolume(store, volume); + await createVolume(store, VOLUME); expect(await store.list("vol-rt")).toEqual([".zattrs", ".zgroup", "annotation/.zattrs", "annotation/.zgroup", "annotation/s0/.zarray"]); - expect(await readVolume(store, "vol-rt")).toEqual(volume); + expect(await readVolume(store, "vol-rt")).toEqual(VOLUME); expect(await store.volumes()).toEqual(["vol-rt"]); // Where Neuroglancer draws the paint: the output voxel size, translated to the volume's offset. const labels = JSON.parse(new TextDecoder().decode(await store.get("vol-rt", "annotation/.zattrs"))); expect(labels.multiscales[0].datasets[0].coordinateTransformations).toEqual([ - { type: "scale", scale: volume.output_voxel_size }, - { type: "translation", translation: volume.dataset_offset_nm }, + { type: "scale", scale: VOLUME.output_voxel_size }, + { type: "translation", translation: VOLUME.dataset_offset_nm }, ]); // Not an annotation volume's .zattrs: none. await store.put("other", ".zattrs", new TextEncoder().encode(JSON.stringify({ multiscales: [] }))); @@ -101,7 +83,7 @@ describe("createVolume and readVolume on a paint store", () => { it("lists the chunks painted and decodes a uint16 chunk little-endian, in C order", async () => { const store = new MemoryPaintStore(); - const volume: PaintVolume = { ...planVolume(DATASET, entry("setup_54"), chain, { id: "vol-chunks", now: NOW }), chunk_size: [1, 2, 2], dtype: "uint16" }; + const volume: PaintVolume = { ...VOLUME, id: "vol-chunks", chunk_size: [1, 2, 2], dtype: "uint16" }; await createVolume(store, volume); // 1, 2, 256, 65535 as little-endian uint16 await store.put(volume.id, `${CHUNK_DIR}3.0.0`, new Uint8Array([1, 0, 2, 0, 0, 1, 255, 255])); diff --git a/web/test/unit/pyodide.ts b/web/test/unit/pyodide.ts index 0914434d..e066747e 100644 --- a/web/test/unit/pyodide.ts +++ b/web/test/unit/pyodide.ts @@ -1,11 +1,15 @@ // cellmap_flow.portable in node, for the tests of what calls it (the rank -// routes): Pyodide from the npm package (the version chunkmirage's workers -// load from jsDelivr), numpy from jsDelivr, build/portable.ts's modules -// written as the page's workers get them, and chunkmirage's own dispatch -// (chunkmirage.calls, from the clone) marshalling each answer as its worker -// does. numpy's wheel needs the network the first time (Pyodide then caches -// it in node_modules/pyodide): without it portablePython() is null, with a -// warning, and the tests that need it skip. +// and Label it routes): Pyodide from the npm package (the version +// chunkmirage's workers load from jsDelivr), numpy from jsDelivr, +// build/portable.ts's modules written as the page's workers get them, and +// chunkmirage's own dispatch (chunkmirage.calls, from the clone) marshalling +// each answer as its worker does. Any other package Pyodide ships (scipy, +// Pillow, ...) is loaded as the worker loads it: not at the start, but the +// first time a call fails to import it, found by the module's name in +// Pyodide's lockfile, and that call is made again. A wheel needs the network +// the first time (Pyodide then caches it in node_modules/pyodide): without +// it portablePython() is null, with a warning, and the tests that need it +// skip. import { readFileSync } from "node:fs"; import { resolve } from "node:path"; @@ -38,8 +42,33 @@ async function load(): Promise { py.runPython('import sys; sys.path.insert(0, "/page")\nfrom chunkmirage import calls\n\ndef call(fn, args, arrays):\n return calls.call(fn, args, [a.to_py() for a in arrays])\n'); const call = py.globals.get("call"); const buffer = (b: Uint8Array) => b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) as ArrayBuffer; + // chunkmirage's pyworker.ts provider: the Pyodide package that provides + // the module `name`, by Pyodide's lockfile (PIL is Pillow's). + const provider = (name: string) => + (Object.values(py.lockfile.packages) as { name: string; imports: string[] }[]).find((p) => p.imports.includes(name))?.name; + // And its withPackages: a package the call could not import (scipy for + // Label it's components, Pillow for an AI annotation's resizes), or one + // imported while it was still being installed, loaded once, then the call + // again. + const loading = new Map>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const withPackages = async (run: () => any) => { + for (let tries = 0; ; tries++) { + try { + return run(); + } catch (e) { + const text = String((e as Error)?.message ?? ""); + const name = (/No module named '(\w+)/.exec(text) ?? /`(\w+)` install you are using seems to be broken/.exec(text))?.[1]; + const pkg = name && provider(name); + if (!name || !pkg || tries > 1) throw e; + if (!loading.has(name)) loading.set(name, py.loadPackage([pkg])); + await loading.get(name); + py.runPython(`import sys\nfor m in [m for m in sys.modules if m == "${name}" or m.startswith("${name}.")]: del sys.modules[m]`); + } + } + }; return async (fn: string, args: Record, arrays: ArrayBuffer[] = []): Promise => { - const out = call(fn, JSON.stringify(args), arrays.map((b) => new Uint8Array(b))); + const out = await withPackages(() => call(fn, JSON.stringify(args), arrays.map((b) => new Uint8Array(b)))); if (typeof out === "string") return JSON.parse(out) as T; const js = out.toJs() as Uint8Array | [string, Uint8Array[]]; out.destroy();