{"name":"napari-turbobox","display_name":"napari-TurboBox","visibility":"public","icon":null,"categories":["Annotation","Visualization"],"schema_version":"0.3.0","on_activate":null,"on_deactivate":null,"contributions":{"commands":[{"id":"napari-turbobox.create_widget","title":"Open BBox Control Panel","python_name":"napari_turbobox._factory:create_control_panel","short_title":null,"category":null,"icon":null,"enablement":null}],"readers":null,"writers":null,"widgets":[{"command":"napari-turbobox.create_widget","display_name":"BBox Control Panel","autogenerate":false}],"sample_data":null,"themes":null,"menus":{},"submenus":null,"keybindings":null,"configurations":{}},"package_metadata":{"metadata_version":"2.4","name":"napari-turbobox","version":"0.1.0","dynamic":["license-file"],"platform":["any"],"supported_platform":null,"summary":"Synchronized multi-viewer 2D/3D bounding-box annotation for napari (>= 0.6.6)","description":"<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/N-Friederich/napari-turbobox/v0.1.0/docs/logo-256.png\" width=\"128\" alt=\"napari-TurboBox logo: a see-through 3D box with three speed lines\">\n</p>\n\n# napari-TurboBox\n\n[![tests](https://github.com/N-Friederich/napari-turbobox/actions/workflows/test.yml/badge.svg)](https://github.com/N-Friederich/napari-turbobox/actions/workflows/test.yml)\n[![PyPI](https://img.shields.io/pypi/v/napari-turbobox.svg)](https://pypi.org/project/napari-turbobox/)\n[![Python versions](https://img.shields.io/pypi/pyversions/napari-turbobox.svg)](https://pypi.org/project/napari-turbobox/)\n[![License](https://img.shields.io/github/license/N-Friederich/napari-turbobox)](https://github.com/N-Friederich/napari-turbobox/blob/main/LICENSE)\n[![codecov](https://codecov.io/gh/N-Friederich/napari-turbobox/graph/badge.svg)](https://codecov.io/gh/N-Friederich/napari-turbobox)\n[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-turbobox)](https://napari-hub.org/plugins/napari-turbobox)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)\n\nnapari-TurboBox is a napari plugin for annotating 2D and 3D images with\naxis-aligned bounding boxes (AABBs). All boxes of an annotation live in one\nshared store. One editable box layer and any number of read-only box layers in\nother napari viewers (for example two orthogonal slice views and a 3D view)\ndisplay that store: every edit is written once and reaches all views, the 2D\nviews on every edit and the 3D views when the edit ends. Mouse edits keep each\nbox axis-aligned and, when the image shape is known, inside the image. The\nboxes can be exported as per-slice COCO or YOLO annotations, for example to\ntrain 2D detectors or to use as per-slice box prompts, or in a native\nNumPy/JSON format.\n\n![Four napari viewers of one C. elegans volume share one box store: XY is editable, XZ, YZ and 3D are read-only. A box that has slipped off its nucleus is dragged back onto it in XY; XZ follows every move, YZ shows the box when it arrives, and the 3D view follows at the release. Its corner is pulled out until the box fits the nucleus. The XY view steps through z and draws only the boxes on each slice. Cmd+Z undoes the fit in every view and Cmd+Shift+Z redoes it. The 3D view is turned.](https://raw.githubusercontent.com/N-Friederich/napari-turbobox/v0.1.0/docs/demo.gif)\n\nA real napari session, recorded by [`docs/make_demo.py`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/docs/make_demo.py):\nevery edit is a Qt mouse or key event on the XY canvas, and the 3D view is\nturned by a mouse drag on its canvas. Before it starts, the script sets the\ncameras and slices and moves one box off its nucleus and shrinks it. The\npointer, the dashed line that marks the XY slice in XZ and YZ, and the cyan\noutline of the edited box are drawn on top. Data: *C. elegans* nuclei, Long et\nal. (2022), doi:[10.5281/zenodo.5942575](https://doi.org/10.5281/zenodo.5942575),\n[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) (see [Application example](#application-example-c-elegans-nuclei)).\n\nDistribution name `napari-turbobox`, import name `napari_turbobox`, licence Apache 2.0.\nArchitecture and API documentation: [`docs/`](https://github.com/N-Friederich/napari-turbobox/tree/v0.1.0/docs) (Sphinx sources).\n\n## Features\n\n- **One shared store.** `BBoxDataStore` holds all boxes as one `(N, 2, D)`\n  `float32` array (`[min corner, max corner]` per box). The layers of a\n  synchronized layout all read this store; the store notifies them when it\n  changes.\n- **Synchronized multi-view layouts.** `create_synchronized_bbox_layers` builds\n  one editable layer in a main viewer and read-only layers in any number of\n  other viewers. Layers in 2D viewers update on every edit, including every\n  mouse-move step of a drag. Layers in viewers that show 3D when the layers are\n  created update once, when the edit session (for example a mouse drag) ends.\n- **Slice-aware drawing.** A 2D view draws only the boxes that intersect its\n  current slice. Editing a single box updates, adds, or removes that box's\n  shape in each view when the incremental path's checks pass; otherwise it\n  rebuilds the view. A slice change or a bulk edit (add, delete, import, undo)\n  rebuilds the view. A 3D view draws every box as a wireframe.\n- **Constrained AABB editing with the mouse.** In the editable layer, dragging a\n  box moves it. Shift-dragging, or grabbing a box within 20 % of its size from\n  an edge, resizes it: on each displayed axis, the side nearer to the grab\n  point follows the mouse. Only the displayed axes change, and every axis keeps\n  an extent of at least one voxel. When the layer knows the image shape, a\n  moved box stops at the image border and resized coordinates are clamped to\n  `[0, shape - 1]`. When boxes overlap, the box with the smallest displayed\n  area under the cursor is grabbed. napari's Transform mode and the\n  non-rectangle drawing tools (ellipse, line, path, polygon, lasso) are\n  switched back to Select mode.\n- **napari's own tools work on single boxes.** napari's *Add Rectangle* adds\n  one box. With a known image shape, a rectangle drawn on a slice is extruded\n  along the hidden axis through the full image depth (`0` to `shape - 1`); a\n  click without dragging\n  adds nothing. napari's *Delete selected shapes* removes exactly the selected\n  box(es). Read-only views ignore edits.\n- **Undo/redo.** The box layer binds Ctrl+Z / Ctrl+Y and Cmd+Z / Cmd+Shift+Z\n  (active while the box layer is selected). On macOS, Cmd+Y is napari's own\n  shortcut for switching between 2D and 3D display, so redo there with\n  Cmd+Shift+Z or Ctrl+Y. The editable layer keeps up to 50 snapshots of all\n  boxes by default (adjustable from 1 to 200 in the control panel). Undo and redo reach\n  all synchronized views.\n- **Control panel** (napari widget *BBox Control Panel*): add a box layer, draw\n  a box, add a box that fits the image or the current viewport, remove all\n  boxes, list, select and delete boxes, export and import.\n- **Export**: native `.npy` / `.json`, COCO JSON (one 2D annotation per slice),\n  YOLO labels (one text file per slice). The same converters are available in\n  Python (`napari_turbobox.export`). See [Export conventions](#export-conventions).\n  Boxes store coordinates only: the control panel exports all boxes as one\n  class, and the Python converters accept an optional class per box\n  (`category_ids`).\n- **Import** of native `.npy` / `.json` box files; imported boxes are validated\n  and appended to the existing boxes. COCO and YOLO files cannot be imported.\n\nThe npe2 manifest contributes one widget (*BBox Control Panel*) and no file\nreaders or writers; images are opened with napari's own readers.\n\n## Installation\n\nRequirements: Python >= 3.10 and napari >= 0.6.6, < 0.10 with a Qt binding\n(PyQt5, PyQt6 or PySide6). napari-turbobox itself depends only on napari,\nNumPy and qtpy.\n\n```bash\npip install \"napari-turbobox[all]\"   # napari-TurboBox + napari with a Qt binding\n```\n\nThe `all` extra installs `napari[all]>=0.6.6,<0.10`; if napari is\nalready installed, `pip install napari-turbobox` is enough.\n\n`napari[all]` installs a Qt binding (PyQt5 with napari 0.6.6, PyQt6 with\nnapari 0.7.0 and 0.9.1) and napari's optional compiled triangulation backend\n(`bermuda`). The napari Shapes layers that napari-TurboBox builds on use that\nbackend when it is installed; napari-TurboBox does not require it. Without any\nQt binding, napari cannot open a viewer.\n\nFrom a checkout of this repository (editable install with the test\ndependencies):\n\n```bash\npip install -e \".[all,testing]\"\npytest\n```\n\nOther extras: `tutorials` (JupyterLab and the file readers the notebooks use),\n`benchmark` (napari-bbox 0.1.1, pandas, matplotlib for `paper_benchmarks/`),\n`dev` (tests, ruff, npe2, build).\n\nThe tests build real napari viewers and therefore need a display (on Linux CI\nthey run under Xvfb).\n\nTested on macOS with:\n\n| napari | Python | Qt binding |\n|---|---|---|\n| 0.6.6 | 3.12 | PyQt6 |\n| 0.7.0 | 3.10 | PyQt5, PyQt6 |\n| 0.7.0 | 3.12 | PyQt6 |\n| 0.9.1 | 3.12 | PyQt6 |\n\nThe full test suite passes in each of these environments (with napari 0.7.0\nand Python 3.10 once per Qt binding). Tests that need the example data or the\noptional packages napari-bbox or hypothesis are skipped when these are\nmissing, and four older widget tests are always skipped because they crash\nin headless runs. On napari 0.6.6 one test is an expected\nfailure: napari's Shapes layer cannot switch a 2D layer back from 3D display\n(see [Limitations](#limitations)). PySide6 was last tested with napari 0.7.0\nat commit 1b2ba33 of the private development repository, before the final\nbenchmark.\n\n## Quick start\n\n### In the napari GUI\n\n1. Open a 2D or 3D image in napari.\n2. In napari's **Plugins** menu, choose **BBox Control Panel** (listed with\n   the plugin's display name in parentheses).\n3. Click **Add Bounding Box Layer**. The new layer takes its shape from the\n   selected image layer (or the top-most image layer), and also its scale and\n   translation when the image has as many dimensions as the viewer. Without an\n   image layer, boxes are not clamped to image bounds.\n4. Click **Draw New BBox (Interactive)**. This switches the layer to napari's\n   *Add Rectangle* mode. Drag a rectangle on the current slice; the box spans\n   the full depth of the hidden axis.\n5. Switch the layer to napari's *Select* mode. Drag a box to move it;\n   Shift-drag it, or grab it near an edge, to resize it.\n6. Delete boxes with napari's *Delete selected shapes*, or select a box in the\n   panel's list and click **Delete Selected**. **Remove All Bounding Boxes**\n   clears the layer. Ctrl+Z / Ctrl+Y (or Cmd+Z / Cmd+Shift+Z on macOS) undo\n   and redo.\n7. **Export BBoxes...** writes native `.npy`/`.json`, a COCO JSON file or a\n   directory of YOLO label files. **Import BBoxes...** appends boxes from a\n   native `.npy`/`.json` file.\n\n**Fit Current Image Layer** adds one box that covers the whole image.\n**Fit Current Viewport** adds a box over the middle half of the displayed axes\nthat is one slice thick along the hidden axes.\n\nThe panel can also be opened from Python:\n`viewer.window.add_plugin_dock_widget(\"napari-turbobox\", \"BBox Control Panel\")`.\n\n![The XY viewer window: napari's layer controls and layer list on the left, the canvas with the selected box in the middle, and the BBox Control Panel on the right with the target layer, the box count, the quick-create buttons, the box list, import and export, and the advanced settings.](https://raw.githubusercontent.com/N-Friederich/napari-turbobox/v0.1.0/docs/screenshot-panel.png)\n\nThe panel manages the box layers of one viewer. Synchronized multi-view layouts\nare set up with the Python API below.\n\n### Python API: four synchronized viewers\n\n```python\nimport napari\nimport numpy as np\n\nfrom napari_turbobox import create_synchronized_bbox_layers\n\nvolume = np.random.default_rng(0).random((60, 256, 256)).astype(np.float32)  # (z, y, x)\n\nxy, yz, xz, v3d = (napari.Viewer(title=t) for t in (\"XY (editable)\", \"YZ\", \"XZ\", \"3D\"))\nfor viewer in (xy, yz, xz, v3d):\n    viewer.add_image(volume, name=\"volume\")\nyz.dims.order = (2, 0, 1)  # displays (z, y); slices along x\nxz.dims.order = (1, 0, 2)  # displays (z, x); slices along y\nv3d.dims.ndisplay = 3      # set BEFORE creating the layers (see Limitations)\n\nlayers = create_synchronized_bbox_layers(\n    main_viewer=xy,\n    sub_viewers=[yz, xz, v3d],\n    image_shape=volume.shape,\n    edge_color=\"cyan\",\n)\n# Boxes are [[z_min, y_min, x_min], [z_max, y_max, x_max]] in voxel indices.\nlayers[0].add_boxes(np.array([[[10, 40, 40], [30, 90, 110]]], dtype=float))\n\nxy.window.add_plugin_dock_widget(\"napari-turbobox\", \"BBox Control Panel\")\nnapari.run()\n```\n\nSignature: `create_synchronized_bbox_layers(main_viewer, sub_viewers,\nbbox_data=None, image_shape=None, sync_mode=\"live\", **layer_kwargs)`. It returns\none layer per viewer: `layers[0]` is the editable layer in `main_viewer`, the\nothers are read-only. `layer_kwargs` (for example `edge_color`, `edge_width`,\n`scale`) are passed to every `TurboBoxLayer`. Each layer's update mode\nis chosen from its viewer's display mode at creation (2D viewer: every edit;\n3D viewer: end of each edit session). `sync_mode=\"snapshot\"` instead creates\nindependent, unsynchronized layers.\n\nBefore validation, `add_boxes` and the `bounding_boxes` setter expand a flat\nhidden axis. With `image_shape` set, that axis spans the full image depth (`0`\nto `shape - 1`). Validation then checks image bounds and the minimum extent:\nwith `image_shape` set, every coordinate must lie in `[0, shape - 1]`, and on\nevery axis `max - min >= 1`; invalid boxes raise `ValueError`. Boxes passed as\n`bbox_data`, and writes made directly to the store (`layer.store`), are not\nvalidated, so prefer `add_boxes`. `layer.bounding_boxes` returns a copy of all\nboxes.\n\nExport from Python:\n\n```python\nfrom napari_turbobox.export import boxes_to_coco, boxes_to_yolo\n\nboxes = layers[0].bounding_boxes                   # (N, 2, 3), float32\ncoco = boxes_to_coco(boxes, volume.shape)          # dict: images, annotations, categories\nyolo = boxes_to_yolo(boxes, volume.shape)          # {\"slice_0010.txt\": \"0 0.292969 0.253906 0.273438 0.195312\\n\", ...}\n```\n\n## Export conventions\n\nBox coordinates are in napari axis order, `(z, y, x)` for 3D and `(y, x)` for\n2D, in the image's data (voxel index) coordinates. COCO and YOLO are 2D\nformats, so 3D boxes are projected (`napari_turbobox/export.py`):\n\n- **Per slice** (default; the only mode offered by the control panel): a 3D\n  box yields one 2D annotation on every integer slice from `ceil(z_min)` to\n  `floor(z_max)` inclusive, all with the same x/y box. A box thinner than one\n  slice (`ceil(z_min) > floor(z_max)`) yields one annotation on slice\n  `round((z_min + z_max) / 2)`. With a known image shape, slice indices are\n  clamped to `[0, Z - 1]`.\n- **Max projection** (Python API only, `mode=\"max_projection\"`): each box yields\n  one annotation of its x/y footprint on a single image.\n- **Axes and size**: x is the last axis, y the second-to-last. The width is\n  `x_max - x_min` and the height `y_max - y_min` (the drawn extent). For a box\n  whose min and max are the first and last voxel index of an object, this is\n  one pixel less than the object's pixel count. Image width and height are the\n  last two axes of the image shape; if no image shape is available, they are\n  derived from the box extents and a warning is issued. Coordinates outside\n  the image are clamped (with a warning).\n- **COCO JSON**: `bbox = [x_min, y_min, width, height]` in pixels,\n  `area = width * height`, `iscrowd = 0`, annotation ids from 1. There is one\n  `images` entry per slice that has at least one annotation: `id` = slice index,\n  `file_name = \"slice_NNNN.png\"` (four-digit zero-padded slice index), `width`,\n  `height`. The exporter writes no image files; you supply the slice images\n  under these names. The default category is `{\"id\": 1, \"name\": \"object\"}`; with\n  `category_ids` (0-based) the COCO `category_id` is `class_id + 1`.\n- **YOLO**: one `slice_NNNN.txt` per slice that has at least one box, one line\n  `class x_center y_center width height` per box, normalized by the image width\n  and height, six decimals. The class defaults to `0`; `category_ids` are used\n  as given.\n- **2D boxes** go to a single image: `image.png` (COCO) / `image.txt` (YOLO).\n- **Native**: `.npy` stores the `(N, 2, D)` `float32` array, `.json` the same\n  array as nested lists. Import reads both formats, requires\n  `D = layer.ndim`, and appends the boxes to the layer.\n\n## Tutorials\n\nThe Jupyter notebooks in [`tutorials/`](https://github.com/N-Friederich/napari-turbobox/tree/v0.1.0/tutorials) run as they are: each\napplication notebook downloads one public volume (verified against its\npublished checksum), derives one box per object, loads the boxes into the\nsynchronized four-viewer layout, replays a mouse drag, and exports COCO and\nYOLO files.\n\n```bash\npip install -e \".[all,tutorials]\"\njupyter lab tutorials/\n```\n\n| Notebook | Modality, data | Boxes |\n|---|---|---:|\n| [`00_napari_turbobox_api`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/tutorials/00_napari_turbobox_api.ipynb) | the Python API on a synthetic volume | – |\n| [`01_confocal_celegans_nuclei`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/tutorials/01_confocal_celegans_nuclei.ipynb) | confocal, *C. elegans* nuclei (Zenodo 5942575) | 555 |\n| [`02_electron_microscopy_nucmm_z`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/tutorials/02_electron_microscopy_nucmm_z.ipynb) | serial-section EM, zebrafish nuclei (NucMM-Z) | 606 |\n| [`03_micro_ct_nucmm_m`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/tutorials/03_micro_ct_nucmm_m.ipynb) | X-ray micro-CT, mouse cortex nuclei (NucMM-M) | 282 |\n| [`04_light_sheet_cellseg3d_mouse_brain`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/tutorials/04_light_sheet_cellseg3d_mouse_brain.ipynb) | light-sheet, cleared mouse brain nuclei (CellSeg3D) | 631 |\n| [`05_instant_sim_polii_clusters`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/tutorials/05_instant_sim_polii_clusters.ipynb) | instant-SIM, nuclei and RNA Pol II clusters (Hajiabadi et al. 2022), boxes from a threshold segmentation | 38 |\n\nEach notebook's counts and a screenshot of the four views are in\n[`tutorials/results/`](https://github.com/N-Friederich/napari-turbobox/tree/v0.1.0/tutorials/results).\n\n## Application example: C. elegans nuclei\n\n`examples/celegans_workflow.py` applies napari-TurboBox to a public\nfluorescence microscopy volume of *C. elegans* nuclei:\n\n- **Dataset**: \"3D nuclei instance segmentation dataset of fluorescence\n  microscopy volumes of C. elegans\" by Long, Peng, Liu, Kim, Myers, Kainmueller\n  and Weigert, Zenodo, doi:[10.5281/zenodo.5942575](https://doi.org/10.5281/zenodo.5942575),\n  licence [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Archive `c_elegans_nuclei.zip`, 84,403,531 bytes, MD5\n  `7d616d91c4a6cce75a7d4dd37d7e666a`. The data are not part of this\n  repository. If you use the data, the record asks you to cite Long et al.\n  (2009), *Nat. Methods* 6:667–672 (raw data); the curated masks are from\n  Hirsch and Kainmueller (2020), MIDL, and the train/val/test split follows\n  Weigert et al. (2020), WACV. The README demo shows a cropped stretch of the\n  first training volume with drawn annotations (pointer, slice line, outline).\n- **Volume**: the first training volume,\n  `c_elegans_nuclei/train/images/C18G1_2L1_1.tif` (140 × 140 × 1244 voxels,\n  `uint8`), with its instance masks\n  `c_elegans_nuclei/train/masks/C18G1_2L1_1.tif` (555 labelled nuclei).\n- **Workflow**: one 3D box per nucleus from the ground-truth masks (minimum and\n  maximum voxel index of each label on every axis), loaded into the\n  four-viewer layout (editable XY view; read-only YZ, XZ and 3D views) and\n  exported as per-slice COCO and YOLO annotations.\n\n```bash\npython examples/celegans_workflow.py          # download (once), derive boxes, export\npython examples/celegans_workflow.py --gui    # open the four-viewer layout instead\n```\n\nThe script downloads the archive once into `--data`, else `$TURBOBOX_DATA`,\nelse `~/.cache/napari-turbobox`, checks its size and MD5 and extracts only this\nvolume and its masks; exports go to `--out`. It uses `tifffile` and `scipy`,\nwhich napari installs. `--gui` opens the control panel through the plugin\nmanifest, so napari-TurboBox must be installed (`pip install -e .`).\n`tests/test_real_data_workflow.py` (marked `slow`) runs the script and compares\nthe boxes, the counts and hashes of the COCO and YOLO files with\n`tests/fixtures/celegans_volume001_expected.json`; it is skipped when the data\nare not available locally, unless `TURBOBOX_ALLOW_DOWNLOAD=1` is set.\n\nWith boxes derived this way, all 555 boxes pass the plugin's validation\nunchanged, and the per-slice export contains 7,262 COCO annotations on 64\nslices (64 COCO images, 64 YOLO files).\n\nThe four views of this volume with all 555 boxes, taken at the end of the demo\nrecording (the cyan outline marks the box that was dragged onto its nucleus\nand fitted to it there):\n\n![XY, XZ and YZ views of a stretch of the C. elegans larva with the boxes drawn on each view's slice, and the 3D view with every box as a wireframe.](https://raw.githubusercontent.com/N-Friederich/napari-turbobox/v0.1.0/docs/screenshot-views.png)\n\n## Generic workflow: from instance labels to boxes\n\n`examples/labels_to_boxes.py` shows the same steps for your own data: an\ninstance label image (background 0, one integer per object) is turned into one\naxis-aligned box per label, the boxes are loaded into a box layer for review\nand correction, and the result is exported with the conventions above.\n\n## Benchmark: update propagation\n\n### What is measured\n\nThe benchmark measures how long one mouse move of a drag takes to reach all\nlive views: the time from the move until every live 2D view has updated its\nshapes and Qt has processed the pending events (`QApplication.processEvents()`\nreturns). This is the CPU-side update time of a warmed process, not the\nend-to-end display latency.\n\n- Both tools are dragged with the mouse in the editable XY view: the press,\n  every move and the release go through napari's own mouse dispatch\n  (`mouse_press_callbacks`, `mouse_move_callbacks`,\n  `mouse_release_callbacks`) into each plugin's own drag handler\n  (`--edit-view mouse`). Only the hit test at the press is fixed, so that both\n  tools drag the same box. The press is not timed; the release is timed\n  separately (below). In both tools the dragged box stays selected and its\n  highlight is redrawn on every move. The events are dispatched at napari's\n  layer level on hidden canvases, so the canvas-level cursor and status-bar\n  updates of a physical mouse are not included for either tool.\n- The viewers are real napari viewers with hidden windows\n  (`napari.Viewer(show=False)`, native Qt platform). No frame is rendered\n  during the measurement, so paint, rasterization and buffer swaps are excluded\n  for both tools.\n- Layer thumbnails are not redrawn during a drag but once at its end, as both\n  tools' own mouse drags do (TurboBox `--thumbnails off`, its default;\n  napari-bbox's synchronized views with its public `block_thumbnail_update()`).\n- The 3D view is updated once per drag, at the release, for both tools. The\n  end-of-drag update at the release (3D view, thumbnails, for TurboBox also\n  the undo entry) is reported separately (`commit_median_ms`) and is not part\n  of the per-update times.\n- The waiting time between moves (30 Hz schedule) is excluded and spent\n  busy-waiting (`--pacing spin`): without rendering, an idle process would\n  otherwise resume from processor idle states before every move, which a\n  rendering napari session does not. A sensitivity run with idle waiting\n  (`--pacing sleep`, k = 3 × 10 drags, panel a at N = 10 / 100 / 1,000 / 10,000\n  and panel b), each replicate followed by the same drags with busy-waiting as\n  a matched control, is written to `paper_results/sleep_pacing/` and\n  `paper_results/sleep_pacing_spin/` and compared in\n  [`paper_results/RESULTS.md`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/paper_results/RESULTS.md) (section \"Pacing sensitivity\"). With idle\n  waiting, TurboBox's median update time is 8.0–8.5 ms in the four-viewer\n  layout (5.3–9.2 ms for 1 to 8 views in panel b), and the speed-ups shrink to\n  1.1–2.3× at N = 10 to 1,000 and 19× at N = 10,000 (matched busy-waiting\n  control: 2.4–10.4× and 80×). At the sampled counts the interactive\n  classification does not change: TurboBox is interactive at all four (99th\n  percentile at most 13.3 ms), napari-bbox at N = 10, 100 and 1,000. N = 2,000\n  and 5,000 were not run with idle waiting.\n\n### Baselines\n\nnapari-bbox has no multi-viewer synchronization, so the harness\n(`paper_benchmarks/sync_adapter.py`) keeps one napari-bbox layer per viewer.\nThe edited XY view is updated by napari-bbox's own drag handler; the other\nviews follow every move:\n\n- **per-box, quiet** (main baseline, `--legacy-sync per_box_quiet`):\n  napari-bbox 0.1.1's public per-box methods (`selected_data`,\n  `remove_selected`, `add`) replace the edited box in every other 2D layer.\n  napari's layer data events are held back during the drag\n  (`layer.events.data.blocker()`) and emitted once per layer at the release, as\n  napari-bbox's own drag does; otherwise every remove and add makes the viewer\n  recompute the layer extents. The box is selected by adding it to the empty\n  selection set, which skips the selection setter's style and display updates;\n  its style is passed to `add()` explicitly. Together this roughly halves\n  napari-bbox's update time compared with `--legacy-sync per_box_nothumb`. The\n  napari-bbox layers are created with napari-bbox's own entry point\n  (`viewer.add_bounding_boxes`). The 3D layer receives the final box at the\n  release.\n- **full reassignment** (`--legacy-sync full --edit-view api`): the whole box\n  array is assigned to every live 2D layer, including the edited one, on every\n  update (`layer.data = boxes`), the mechanism of napari's multiple-viewer\n  example (which, driven by napari-bbox's data events, would update the other\n  views only at the start and end of a drag); the 3D layer is reassigned at the\n  release.\n\n### Protocol\n\n- Volume 60 × 256 × 256 voxels (blank image), boxes of 18 × 18 × 18 voxels at\n  seeded random positions (seed 42), identical for both tools.\n- Drag protocol `xy_visible`: each drag picks one box that crosses the editing\n  XY slice (z = 30) and moves it in y and x only, alternately translating it by\n  up to ±20 voxels and resizing it (moving its max corner) by up to ±15 voxels\n  per axis, in 60 linearly interpolated sub-edits on a 30 Hz schedule. The drag sequence is seeded by\n  the drag index and is identical for both tools. Each configuration starts\n  with one untimed warm-up drag (`--warmup 1`), after which the boxes are\n  restored; it is not saved.\n- Panel a (four-viewer layout): editable XY view, read-only YZ and XZ views\n  (all live) and one 3D view (updated at the end of each drag), with\n  N = 10, 50, 100, 200, 500 and 1,000 boxes.\n- Panel b (view scaling): V = 1, 2, 4 and 8 identical XY views at N = 100, no\n  3D view.\n- Main run: 50 drags × 60 sub-edits per configuration, k = 5 replicate\n  processes per tool, the two tools interleaved per replicate.\n- Large-N run: panel a with N = 2,000, 5,000 and 10,000, 50 drags per\n  configuration, k = 5.\n- Full-reassignment baseline: panel a with N <= 1,000, 10 drags, k = 3\n  (napari-bbox only).\n- Statistics: per run, the median, 95th and 99th percentile of all sub-edit\n  times; per configuration, the median of the k run medians with a 95 %\n  percentile-bootstrap interval over the run medians (10,000 resamples; for\n  one tool's median with k = 3 or 5 runs, this interval equals the range of\n  the run medians), and the median of the k per-run 95th and 99th\n  percentiles. The speed-up is the ratio of the two medians, with a 95%\n  percentile-bootstrap interval from 10,000 ratios of resampled run medians\n  (`drag_session_speedup.csv`, N <= 1,000); this interval is not the range of\n  the run medians. A configuration counts as interactive when its median and\n  its 95th and 99th percentiles (each the median over the k runs) are all at\n  most 33 ms, the 30 Hz CPU update budget marked in the figure.\n- Environment (one environment for both tools): Python 3.12.13, napari 0.7.0,\n  napari-bbox 0.1.1, PyQt6 6.10.2, NumPy 2.4.3, vispy 0.16.1, bermuda 0.1.7;\n  Apple M2 Pro, macOS 27.0, native Cocoa Qt platform, display attached,\n  viewer windows hidden. Every run also writes a `.meta.json` file with the\n  package versions, Qt binding, triangulation backend, git commit and\n  timestamps. Exact environment file:\n  `paper_results/environment/pip_freeze_benchmark_env.txt`. A local path and a\n  private repository in it were redacted after the run. The sha256 of the file as it was during\n  the run is `5d3c862b…` (see `paper_results/environment/README.md`). Every\n  drag-run sidecar records this digest and the code state\n  at the start and end of the run; the memory and image-size sidecars record\n  the start and end code states and the reference-environment check. Code\n  state of the final run (25 Sep 2026, 13:00–19:42): `code_state.combined_sha256`\n  `a8ad8214…` at the start and end of every run, `src/napari_turbobox/layer.py`\n  sha256 `c14ade67…`. The measured `src/`, `paper_benchmarks/`, `tests/` and run\n  scripts are archived in `paper_results/provenance/benchmarked_code_state_20260925.tgz`\n  (sha256 `64706c5a…`, without bytecode caches and build metadata). The absolute\n  repository path in two run scripts was redacted, before that the sha256 was\n  `fd9e2469…`. The raw snapshot (sha256 `e2e2a41d…`) is kept by the authors.\n  The measured code was never committed. It was the working tree on top of\n  commit 1b2ba33 of the private development repository, and\n  `paper_results/provenance/code_state_vs_1b2ba33.patch` is its diff against\n  that commit. The code-state digest covers `src/` and `paper_benchmarks/`,\n  and the code-state and `layer.py` hashes above belong to that archive. The plugin code in this\n  repository differs from it in four functions (see `docs/changelog.rst`):\n  - In `TurboBoxLayer._incremental_shape_update`, a new `except` branch marks\n    the view for a rebuild when the in-place edit raises. This function runs\n    on every timed TurboBox update, but the new branch runs only on an error.\n  - `UndoStack.get_undo_description` now returns the label of the action that\n    undo reverts.\n  - `TurboBoxLayer.undo` reads that label before undoing and logs it;\n    `redo` changed only in its log message.\n- Other changes since the final run: comments and docstrings in `src/` were\n  revised. `tests/test_undo.py` and `tests/test_incremental_enter_leave.py`\n  gained tests. `test_undo.py` now expects the corrected undo label, and\n  `test_incremental_enter_leave.py` also checks that the mesh vertices are\n  finite (except in the zero-extent test, where napari's own triangulation\n  produces NaN for flat boxes).\n  `paper_benchmarks/make_final_figures.py` changed in the panel (c) legend\n  and y-range and the y-axis label (\"CPU-side update time\", panels a, b, e\n  and f) of Figure 3 and in its docstrings and comments, and panel (e)\n  now plots all 50 measured drags per run (before, it left out the first one).\n  Its `--update-paper-figures` option was removed, and its default input and\n  output directories are now `paper_results/main` and\n  `benchmark_results/figures`. In `drag_session_benchmark.py`, the module\n  docstring, the path of the environment file in one comment and one warning\n  message, one further comment and the default `--out` (now\n  `benchmark_results`) changed, and the process-state record skips\n  `os.nice` and `os.getloadavg` on Windows, where they do not exist; in `memory_benchmark.py`, the module docstring\n  and two comments. `sync_adapter.py`\n  is unchanged. Five scripts of an earlier benchmark design that the final run\n  did not use (`run_benchmarks.sh`, `resolution_benchmark.py`,\n  `generate_paper_screenshots.py`, `generate_gui_screenshots.py`,\n  `interactive_celegans_multiview.py`) and `src/requirements/legacy.txt` were\n  removed. The run scripts moved to `paper_benchmarks/final_run/`\n  (`run_final_v2.sh` is now `run_final.sh`). `run_final.sh` and `run_post.sh`\n  now find the repository from their own location, require the Python in `BB`,\n  take the results directory from `OUT`, write their logs to `benchmark_logs/`,\n  and `run_final.sh` records the code state against `HEAD` and the pip freeze\n  of `BB` in `OUT`; `run_final.sh` takes its stop file from `benchmark_logs/STOP`\n  and moves invalid runs to `OUT/_invalid/`. `with_lock.py` (not in the archive,\n  like `sleep_check.py`) keeps its lock file in `benchmark_logs/`. `memory_final.py` and\n  `resolution_final.py` changed only in their default output directory and the\n  script path they record (`resolution_final.py` also in one docstring line).\n  `report_final.py`, which is not in the archive, gained the checks against\n  the design described under Reproducing, reads `paper_results/` and accepts\n  the redacted environment file through\n  `paper_results/environment/freeze_redaction.json`.\n  Otherwise only docstrings, comments and descriptive strings changed (six test\n  docstrings and two test comments, the fixture's provenance text, the\n  `src/requirements/modern.txt` header, the control panel's window title, now\n  \"napari-TurboBox\" instead of \"nD Bounding Boxes\"). The plugin manifest\n  `src/napari_turbobox/napari.yaml` gained the categories Annotation and\n  Visualization, and the tutorial helper `show_path` keeps local paths out of\n  the printed output. The sidecars of the reported run\n  still name the former locations of the run scripts and of the benchmark\n  environment (under `records/` in the development checkout). Host names and\n  absolute paths of the benchmark machine were redacted in the sidecars, in the\n  environment file and in two archived run scripts before publication (see\n  `paper_results/environment/README.md`). The logs of the reported run\n  (driver, power and lock logs) are kept by the authors.\n\n### Results\n\nMedian time per update in ms [95 % interval], panel a (four-viewer layout).\n\n| N boxes | napari-TurboBox | napari-bbox 0.1.1, per-box quiet | Speed-up |\n|---:|---|---|---|\n| 10 | 1.6 [1.6–1.7] | 3.9 [3.9–3.9] | 2.4× [2.4–2.4] |\n| 100 | 1.7 [1.7–1.7] | 5.3 [5.2–5.3] | 3.2× [3.1–3.2] |\n| 1,000 | 1.7 [1.7–1.7] | 18.0 [17.8–18.2] | 10.5× [10.4–10.6] |\n| 10,000 * | 2.0 [2.0–2.0] | 158.7 [155.6–159.1] | 79.1× [77.5–80.2] |\n\n\\* Large-N run (N = 2,000–10,000), same protocol: 5 runs × 50 drags.\n\nFull-reassignment baseline (napari-bbox 0.1.1, k = 3, 10 drags, updates written without mouse events), compared with\nTurboBox's median from the main run:\n\n| N boxes | napari-bbox, full reassignment | Ratio to TurboBox |\n|---:|---|---|\n| 10 | 23.1 [23.1–23.2] | 14× |\n| 100 | 112.2 [112.0–112.4] | 67× |\n| 1,000 | 908.6 [907.8–909.3] | 531× |\n\nLargest tested N at which the median, 95th and 99th percentile stay within 33 ms:\nnapari-TurboBox 10,000 (the largest N tested; 99th percentile 4.4 ms), napari-bbox (per-box)\n1,000 (at 2,000: median 32.4 ms, 95th/99th percentile 33.5/35.1 ms). The 95th/99th percentiles,\nthe end-of-drag 3D update times and the view-scaling results (panel b) are in\n`paper_results/main/drag_session_summary.csv`.\n\n### Reproducing\n\nCreate one environment for both tools. The versions above were used for the\nreported run; pin them to reproduce it exactly.\n\n```bash\npip install \"napari[all]==0.7.0\" \"napari-bbox==0.1.1\" pandas matplotlib scipy\n```\n\nRun from the repository root (TurboBox is imported from `src/`, as in the\nreported run; the scripts in `paper_benchmarks/final_run/` also import\n`paper_benchmarks` from the root). These are the commands of the final run\nscript. They write to `$OUT` (here `benchmark_results/`, which git ignores),\nso the reported files in `paper_results/` stay unchanged. Run them in bash: zsh, the default shell on macOS, does not split\nunquoted variables such as `$H` into words, so the commands fail there. Start\n`bash` first.\n\n```bash\nexport PYTHONPATH=\"$PWD/src:$PWD\"\nOUT=benchmark_results\nH=\"-m paper_benchmarks.drag_session_benchmark --drag-protocol xy_visible --ortho-follow --warmup 1 --pacing spin --edit-view mouse\"\nTB=\"--tool modern --thumbnails off\"\nLG=\"--tool legacy --legacy-sync per_box_quiet\"\n\n# 1) Large N: panel a, N = 2,000 / 5,000 / 10,000, k = 5 x 50 drags\nfor r in 0 1 2 3 4; do\n  python $H $TB --panels a --ns 2000,5000,10000 --replicate $r --out $OUT/large_n\n  python $H $LG --panels a --ns 2000,5000,10000 --replicate $r --out $OUT/large_n\ndone\n\n# 2) Main run: panels a and b, k = 5 x 50 drags, tools interleaved per replicate\nfor r in 0 1 2 3 4; do\n  python $H $TB --replicate $r --out $OUT/main\n  python $H $LG --replicate $r --out $OUT/main\ndone\n\n# 3) Pacing sensitivity: idle waiting between moves, k = 3 x 10 drags, each replicate followed by the\n#    same drags with busy-waiting as a matched control\nfor r in 0 1 2; do\n  python ${H/--pacing spin/--pacing sleep} $TB --ns 10,100,1000,10000 --drags 10 --replicate $r --out $OUT/sleep_pacing\n  python ${H/--pacing spin/--pacing sleep} $LG --ns 10,100,1000,10000 --drags 10 --replicate $r --out $OUT/sleep_pacing\n  python $H $TB --ns 10,100,1000,10000 --drags 10 --replicate $r --out $OUT/sleep_pacing_spin\n  python $H $LG --ns 10,100,1000,10000 --drags 10 --replicate $r --out $OUT/sleep_pacing_spin\ndone\n\n# 4) Full-reassignment baseline: panel a, N <= 1,000, k = 3 x 10 drags\nfor r in 0 1 2; do\n  python -m paper_benchmarks.drag_session_benchmark --drag-protocol xy_visible --ortho-follow --warmup 1 \\\n      --pacing spin --tool legacy --legacy-sync full --edit-view api --panels a --drags 10 \\\n      --replicate $r --out $OUT/tutorial_full\ndone\n```\n\n`paper_benchmarks/final_run/run_final.sh` runs these blocks with\n`TB_THUMBS=off BB_SYNC=per_box_quiet PACING=spin EDIT_VIEW=mouse` (stages\n`large main sleep post tut`); it waits\nfor AC power, keeps the machine awake, serializes all processes with a lock\nand checks the power log after every process. It runs on macOS only (`pmset`,\n`caffeinate`), finds the repository from its own location, writes to `OUT`\n(default: `benchmark_results/`; a relative path is taken from the repository\nroot) and its logs to `benchmark_logs/`, skips runs whose result file already\nexists, and records the code state and the pip freeze of the environment in\n`OUT`. `BB` must name the Python of the benchmark environment, for example\n`BB=$(which python) TB_THUMBS=off BB_SYNC=per_box_quiet PACING=spin EDIT_VIEW=mouse paper_benchmarks/final_run/run_final.sh`.\nOn other systems, use the commands above.\n\nReported wall time on the machine above: large N 1.8 h, main run 2.9 h, pacing sensitivity with its\nbusy-waiting control 0.9 h, memory and image size 4 min, full-reassignment baseline 1.0 h (6.7 h in total).\n\nEach run writes `drag_session_<tag>_rep<r>.csv` (one row per configuration:\nper-run median, 95th and 99th percentile of the sub-edit times and the median\nend-of-drag 3D update time), a `.meta.json` provenance file and, for panel a\nat N = 100 and panel b at V = 4, `drag_session_dist_<tag>_rep<r>.csv` with every\nsub-edit time. The tags are `modern-xy_visible-follow-warm1-spin-mouse`,\n`legacy-per_box_quiet-xy_visible-follow-warm1-spin-mouse` and\n`legacy-xy_visible-follow-warm1-spin`; the runs with idle waiting\n(`sleep_pacing/`) have the same tags without `-spin`.\n\nThe memory panel (c) and the image-size panel (f) come from two shorter\nmeasurements in the same environment, run after blocks 1 to 3: memory for\nboth tools (`paper_benchmarks/final_run/memory_final.py`, which runs\n`paper_benchmarks/memory_benchmark.py` once per plugin) and image size for\nTurboBox (`paper_benchmarks/final_run/resolution_final.py`, the drag session\nof the harness at five image sizes with the same drag protocol and thumbnail\nsetting). `paper_benchmarks/final_run/run_post.sh` runs both:\n\n```bash\npython paper_benchmarks/final_run/memory_final.py $OUT/memory\nTB_THUMBS=off PACING=spin EDIT_VIEW=mouse python paper_benchmarks/final_run/resolution_final.py $OUT/resolution\n```\n\nAggregation and figure: the command below writes `drag_session_summary.csv`,\n`drag_session_speedup.csv` and `Figure3_performance.{png,pdf}` into `--out`.\nIt reads `benchmark_memory*.csv` and `benchmark_resolution.csv` from the\nresults directory, so they are copied there first. `--large-n` adds the\nlarge-N configurations to panels (a) and (d) as open markers and also\n(re)writes the large-N run's own `drag_session_summary.csv` and\n`drag_session_speedup.csv` in its directory. With\n`paper_results` in place of `$OUT`, the same command rebuilds the reported\ntables and figure from the reported per-run files (the memory and image-size\nfiles are already in `paper_results/main/`).\n\n```bash\ncp $OUT/memory/benchmark_memory*.csv $OUT/resolution/benchmark_resolution.csv $OUT/main/\npython -m paper_benchmarks.make_final_figures --results $OUT/main --out $OUT/main \\\n    --large-n $OUT/large_n\n```\n\nAll numbers of the tables above (including the large-N and full-reassignment\nratios and the interactive limits) are collected by\n`paper_benchmarks/final_run/report_final.py` into [`paper_results/RESULTS.md`](https://github.com/N-Friederich/napari-turbobox/blob/v0.1.0/paper_results/RESULTS.md) and\n`paper_results/results_numbers.json`. It aggregates each directory with the\nsame `aggregate()` function and re-seeds the bootstrap before each directory,\nas a fresh process would. Before that it checks every stage against the design\nabove (all runs, replicates and configurations, drag counts, pacing, one code\nstate for all runs, the environment digest, AC power) and stops without\nwriting anything if a value is missing or differs. `TB_RESULTS=$OUT` checks\nand aggregates a re-run instead; its runs must also come from the reference\nenvironment.\n\n```bash\npython paper_benchmarks/final_run/report_final.py\n```\n\nFor a quick functional check, run a short smoke configuration, for example\n`python -m paper_benchmarks.drag_session_benchmark --drag-protocol xy_visible --edit-view mouse --tool modern --panels a --ns 10,100 --drags 2 --replicate 0 --out smoke_out`.\n\nTimings depend on hardware, operating system and display; re-running the\naggregation on the same per-run CSVs reproduces the tables exactly.\n\n## Limitations\n\n- Boxes have 2 or 3 spatial axes (`D = 2` or `3`). Time and channel axes are\n  not supported.\n- The depth of a box (its extent along the axis hidden in the editing view)\n  cannot be dragged in the read-only orthogonal views, and new rectangles span\n  the full depth. To set it with the mouse, reorient the editable viewer so that\n  it displays the depth axis (for example `viewer.dims.order = (1, 0, 2)` for an\n  XZ view) and drag or Shift-drag the box there; or set it through the API\n  (`layer.bounding_boxes = ...`) or by importing a box file.\n- Clamping to the image needs the image shape (`image_shape`, or an image layer\n  when the layer is added from the control panel). Without it, only the\n  minimum extent of one voxel per axis is enforced, and a rectangle drawn with\n  Add Rectangle is not extruded through the full depth.\n- A layer's update mode (every edit vs. end of edit session) is decided from\n  its viewer's display mode when the layer is created. If a viewer is switched\n  between 2D and 3D later, set `layer.sync_mode` (`\"live\"` or `\"on_commit\"`)\n  yourself.\n- The benchmark excludes rendering because the viewers are hidden. Mouse\n  events are dispatched at the layer level into each tool's own drag handler,\n  including selection-highlight updates; canvas-level cursor and status-bar\n  updates are excluded. End-of-drag times include the 3D view update,\n  thumbnails, and, for TurboBox, the undo snapshot.\n- Boxes carry no class label. Per-box classes can be passed to the Python\n  converters (`category_ids`) but are not stored with the boxes, and COCO or\n  YOLO files cannot be imported.\n- A 2D box layer (`D = 2`) keeps showing its rectangles when the viewer is\n  switched to 3D display on napari >= 0.7. On napari 0.6.x, napari's Shapes\n  layer cannot be switched back from 3D display in that case (plain Shapes has\n  the same limitation).\n- Each undo snapshot is a full copy of all boxes.\n- The data store is not thread-safe; modify boxes from the main (Qt) thread.\n\n## Repository layout\n\n- `src/napari_turbobox/`: the plugin (store, layer, multi-view factory, control\n  panel, export, spatial index, undo)\n- `tests/`: test suite (pytest, pytest-qt)\n- `examples/`: `celegans_workflow.py` and `labels_to_boxes.py` (see above) and\n  `multi_viewer_demo.py` (three synchronized viewers on a synthetic volume)\n- `docs/`: the logo (`logo.svg`; `logo-small.svg` for 32 px and below,\n  `logo-mono.svg` in one colour, `logo-256.png`), the README demo (`demo.gif`,\n  `screenshot-*.png`) and its recorder `make_demo.py`, and the Sphinx sources:\n  architecture (store, update path, spatial index, export), API reference,\n  contributing guide, changelog\n- `paper_benchmarks/`: benchmark harness, synchronization adapters and\n  aggregation/figure script; `paper_benchmarks/final_run/`: the scripts that\n  ran the final benchmark and collect its numbers\n- `paper_results/`: per-run records, summary tables, Figure 3 and the\n  environment and code-state files of the benchmark described above\n- `tutorials/`: the tutorial notebooks and their recorded results\n\n## Citation\n\nIf you use napari-TurboBox, please cite it using the metadata in\n`CITATION.cff` (GitHub's \"Cite this repository\" button reads this file).\nAn Application Note on napari-TurboBox is in preparation.\n\n## Licence\n\nApache License 2.0. See `LICENSE` and `NOTICE`.\n","description_content_type":"text/markdown","keywords":"napari,plugin,bounding-box,annotation,microscopy,volumetric,3D,COCO,YOLO","home_page":"https://github.com/N-Friederich/napari-turbobox","download_url":null,"author":"Nils Friederich","author_email":"nils.friederich@kit.edu","maintainer":null,"maintainer_email":null,"license":"Apache-2.0","classifier":["Development Status :: 3 - Alpha","Framework :: napari","Intended Audience :: Science/Research","License :: OSI Approved :: Apache Software License","Operating System :: OS Independent","Programming Language :: Python :: 3","Programming Language :: Python :: 3.10","Programming Language :: Python :: 3.11","Programming Language :: Python :: 3.12","Topic :: Scientific/Engineering :: Image Processing","Topic :: Scientific/Engineering :: Visualization"],"requires_dist":["napari<0.10,>=0.6.6","numpy","qtpy","napari[all]<0.10,>=0.6.6; extra == \"all\"","pytest>=7.0.0; extra == \"testing\"","pytest-qt>=4.2.0; extra == \"testing\"","hypothesis>=6.0.0; extra == \"testing\"","jupyterlab; extra == \"tutorials\"","h5py; extra == \"tutorials\"","tifffile; extra == \"tutorials\"","scikit-image; extra == \"tutorials\"","scipy; extra == \"tutorials\"","imageio; extra == \"tutorials\"","napari-bbox==0.1.1; extra == \"benchmark\"","pandas; extra == \"benchmark\"","matplotlib; extra == \"benchmark\"","scipy; extra == \"benchmark\"","pytest>=7.0.0; extra == \"dev\"","pytest-qt>=4.2.0; extra == \"dev\"","hypothesis>=6.0.0; extra == \"dev\"","pytest-cov>=4.0.0; extra == \"dev\"","ruff>=0.4.0; extra == \"dev\"","npe2; extra == \"dev\"","build; extra == \"dev\"","twine; extra == \"dev\""],"requires_python":">=3.10","requires_external":null,"project_url":["Bug Tracker, https://github.com/N-Friederich/napari-turbobox/issues","Documentation, https://github.com/N-Friederich/napari-turbobox#readme","Tutorials, https://github.com/N-Friederich/napari-turbobox/tree/main/tutorials","Source Code, https://github.com/N-Friederich/napari-turbobox"],"provides_extra":["all","testing","tutorials","benchmark","dev"],"provides_dist":null,"obsoletes_dist":null},"npe1_shim":false}