Metadata-Version: 2.4
Name: isoview
Version: 0.1.5
Summary: Multi-view light sheet microscopy image processing pipeline
Project-URL: Homepage, https://github.com/MillerBrainObservatory/isoview
Project-URL: Repository, https://github.com/MillerBrainObservatory/isoview
Author: Miller Brain Observatory
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Image Processing
Requires-Python: ==3.12.9
Requires-Dist: cellpose>=3.0.0
Requires-Dist: dask[array]>=2024.1.0
Requires-Dist: fsspec>=2023.12.0
Requires-Dist: glfw>=2.8.0
Requires-Dist: h5py>=3.10.0
Requires-Dist: imageio>=2.31.0
Requires-Dist: ipywidgets>=8.1.0
Requires-Dist: jupyterlab
Requires-Dist: jupyterlab-vim>=4.1.0
Requires-Dist: matplotlib>=3.7.0
Requires-Dist: mbo-fastplotlib[imgui,notebook]
Requires-Dist: mbo-utilities[all]>=2.6.0
Requires-Dist: numpy<3.0,>=1.26.0
Requires-Dist: ome-zarr>=0.9.0
Requires-Dist: opencv-python>=4.8.0
Requires-Dist: pyklb>=0.3.0
Requires-Dist: scikit-image>=0.22.0
Requires-Dist: scikit-learn>=1.3.0
Requires-Dist: scipy>=1.11.0
Requires-Dist: tifffile>=2023.7.0
Requires-Dist: tqdm>=4.66.0
Requires-Dist: trackpy>=0.7
Requires-Dist: xmltodict>=0.13.0
Requires-Dist: zarr>=3.0.0
Provides-Extra: dev
Requires-Dist: mypy>=1.5.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: ruff>=0.0.280; extra == 'dev'
Description-Content-Type: text/markdown

# IsoView Light Sheet Microscopy Pipeline

## Acquisition Modes

Raw input is always flat — `SPC##_TM#####_ANG###_CM#_CHN##_PH#.stack` files in a single directory.
Mode is auto-detected from SPC/TM counts:

| Condition | Mode | Description |
|-----------|------|-------------|
| Multiple TM values | timelapse | time series, any number of specimens |
| Single TM + multiple SPC | tiled | spatial tiles, one timepoint |
| Single TM + single SPC | single | treated as timelapse with 1 timepoint |

## Filename Tags

| Tag | Meaning | Raw | Corrected |
|-----|---------|-----|-----------|
| `SPC##` / `SPM##` | specimen / tile | `SPC00` | `SPM00` |
| `TM#####` | timepoint | `TM00000` | `TM000000` |
| `CM#` | camera | `CM0` | `CM00` |
| `CHN##` | channel (raw) | `CHN00`, `CHN01` | — |
| `VW##` | view (corrected) | — | `VW00` (z-scan), `VW90` (y-scan) |
| `ANG###` | illumination angle | `ANG000` | — (dropped) |
| `PH#` | phase | `PH0` | — (dropped) |

## Output Layout

Correction writes to `{input_dir.name}{corrected_suffix}` (default `.corrected`)
and fusion writes to a sibling `{input_dir.name}{fused_suffix}` (default
`.fused`), both as siblings of the raw input directory.

### Correction (`correct_stack.py`)

| Mode | Path | Filename |
|------|------|----------|
| Timelapse | `root.corrected/SPM00/TM000000/` | `SPM00_TM000000_CM00_VW00.ome.tif` |
| Tiled | `root.corrected/SPM00/` | `SPM00_CM00_VW00.ome.tif` |

Each tile will have a `SPM0N` folder, where `N` is the tile index.

### Fusion (`multi_fuse`)

| Mode | Path | Filename |
|------|------|----------|
| Timelapse | `root.fused/adaptive/SPM00/TM000000/` | `SPM00_TM000000_CM00_CM01_VW00.ome.tif` |
| Tiled | `root.fused/adaptive/SPM00/` | `SPM00_CM00_CM01_VW00.ome.tif` |

Default pairs: `[(0, 1), (2, 3)]` — cameras 0,1 share CHN00/VW00; cameras 2,3 share CHN01/VW90.
Only the second camera in each pair gets rotation/flip transforms.

## Supported Output Formats

| Format | Extension | Notes |
|--------|-----------|-------|
| OME-TIFF | `.ome.tif` | with metadata, optional resolution pyramids |
| Zarr v3 | `ome.zarr` | OME-NGFF metadata |
| KLB | `.klb` | Keller Lab Block (bzip2) |

## XML Metadata

`read_xml_metadata(xml_path)` parses one `push_config` XML; `read_all_xml_metadata(input_dir, specimen)` returns `(common, per_camera)` — fields equal across cameras vs camera-specific.

| Field | Type | Source | Notes |
|-------|------|--------|-------|
| `data_header` | str | `@data_header` | acquisition session label |
| `specimen_name` | str | `@specimen_name` | |
| `timestamp` | str | `@timestamp` | acquisition datetime |
| `time_point` | int | `@time_point` | |
| `specimen_XYZT` | str | `@specimen_XYZT` | also parsed → `stage_x/y/z` (float, µm) |
| `angle` | float | `@angle` | |
| `camera_index` | str | `@camera_index` | comma-separated per camera |
| `camera_type` | str | `@camera_type` | |
| `camera_roi` | str | `@camera_roi` | |
| `wavelength` | str | `@wavelength` | emission, per camera |
| `illumination_arms` | str | `@illumination_arms` | per camera |
| `illumination_filter` | str | `@illumination_filter` | |
| `exposure_time` | float | `@exposure_time` | ms |
| `detection_filter` | str | `@detection_filter` | |
| `detection_objective` | str | `@detection_objective` | also parsed → `objective_mag` (float) |
| `dimensions` | ndarray | `@dimensions` | `(n_cameras, 3)` from `"WxHxD,WxHxD,…"` |
| `z_step` | float | `@z_step` | µm; VW00 (Z-scan) |
| `y_step` | float | `@y_step` | µm; VW90 (Y-scan) |
| `stack_direction` | str | `@stack_direction` | drives `camera_view_map` |
| `planes` | str | `@planes` | |
| `laser_power` | str | `@laser_power` | |
| `experiment_notes` | str | `@experiment_notes` | |
| `zplanes` | int | derived | `dimensions[0][-1]` |
| `fps` | float | derived | `1000.0 / exposure_time` |
| `vps` | float | derived | `fps / zplanes` |
| `camera_pixel_size_um` | float | constant | `6.5` (Hamamatsu C11440-22C) |
| `pixel_resolution_um` | float | derived | `camera_pixel_size_um / objective_mag` |
| `axial_step` | float | merged | unifies `z_step` / `y_step` across XMLs |
| `camera_view_map` | dict | synthesized | from `stack_direction`: Z-scan → `{0:0,1:0}`, Y-scan → `{2:90,3:90}` |

XML discovery order (in `read_all_xml_metadata`): `ch*_spec{NN}.xml` → `ch*.xml` → `*_CHN*.xml` → `*_VW*.xml` (legacy), also under `SPM##/`. Channel inferred from filename via `ch(\d+)` → `CHN(\d+)` → `VW(\d+)`.

## Companion Files

For each corrected/fused volume `<base>.<ext>`:

| File | Location | Format |
|------|----------|--------|
| `<base>.segmentationMask.<ext>` | corrected dir | 3D mask, same ext as volume |
| `<base>.xyMask.<ext>`, `<base>.xzMask.<ext>` | corrected dir | 2D per-axis masks |
| `<base>.{xy,xz,yz}Projection.tif` | corrected/fused dir | 2D projection |
| `<base>.minIntensity.npz` | corrected dir | numpy archive |
| `<base>.mask.<ext>` | fused dir | 3D segmentation |
| `<base>.fusionMask.tif` | fused dir | 2D blending mask |
| `Background_*.tif` | raw root | shared per dataset; `read_background_values(files, percentile=3.0)` → list of floats |
| `SPM##[_TM######]_CHN##.xml` / `_VW##.xml` (legacy) | corrected dir | per-channel XML copy |

## Project Contents

`detect_project_contents(root, corrected_suffix=".corrected", specimen=0)` returns a single tagged manifest:

```python
# timepoint mode
{"mode": "timepoint", "root": Path,
 "backgrounds": [Path, ...],
 "timepoints": {tp_int: {
    "raw_dir": Path,
    "raw_stacks": {(cam, chn): Path},
    "xml": {ch: Path},
    "corrected": {                  # or None
        "dir": Path,
        "volumes": {cam: Path},
        "masks": {cam: {"segmentation": Path, "xy": Path, "xz": Path}},
        "projections": {cam: {"xy": Path, "xz": Path, "yz": Path}},
        "min_intensity": {cam: Path},
        "xml": {ch: Path},
    },
    "fusion": {method: {"dir": Path,
        "pairs": {(cam0, cam1, vw): {
            "fused": Path, "mask": Path, "fusion_mask": Path,
            "projections": {"xy": Path, "xz": Path, "yz": Path},
        }}}},
 }}}

# tiled mode
{"mode": "tiled", "root": Path,
 "tiles": {"SPM00": {<same as timepoint entry, plus "backgrounds">}}}
```

## `.stack` Reading

Raw `.stack` is little-endian `uint16`, shape `(D, H, W)`, memmappable. `(W, H)` from XML `@dimensions[0]`; `D` derived from file size (`stat().st_size // 2 // (H*W)`) — XML's `D` may be stale on aborted acquisitions.

## `isoview_config.json`

Written next to data; tracks microscope params + every correction/fusion run.

```python
{
  "paths": {"raw": str, "corrected": str},
  "microscope": {                   # from XML + ProcessingConfig
    "pixel_spacing_camera": 6.5, "pixel_spacing_xy": float, "pixel_spacing_z": float,
    "detection_objective_mag": float, "camera_view_map": {str: int}, "camera_pairs": [[int,int]],
    "wavelength", "exposure_time", "fps", "specimen_name",
  },
  "corrections": {"<label>": {"config_diff": {...}, "run": {"isoview_version",
                  "started_at", "completed_at", "elapsed_seconds", "status", "summary"}}},
  "fusions":     {"<label>/<blending>": {"config_diff": {...}, "run": {...}}},
}
```

Bootstrap from `correction_run.json` / `fusion_run.json` / `*.fused/*/run.json` via `IsoviewConfig.from_directory(root)`.

## Quickstart

1. `isoview init <path/to/raw>` — scaffolds a `scripts/` folder next to the raw data with `correct_stack.py`, `multi_fuse.py`, `pipeline.ipynb`, and `parameters.md`.
2. Open `scripts/` in VSCode and run `correct_stack.py` then `multi_fuse.py`, or open `pipeline.ipynb` and run both cells.
3. Each step appends its config + `isoview_version` to `isoview_config.json` next to the data, so every run is reproducible.

See [examples/compression_demo.py](examples/compression_demo.py) for a full end-to-end run on 10 timepoints. Full parameter reference: [parameters.md](parameters.md).

## Usage

Entrypoints: `pipeline/correct_stack.py` (correction) and `pipeline/multi_fuse.py` (fusion).

```python
from pathlib import Path
from isoview import ProcessingConfig, correct_stack, multi_fuse

config = ProcessingConfig(
    input_dir=Path(r"E:\isoview\dataset"),
    # specimens=None,                       # auto-detect from SPC## in filenames
    # timepoints=None,                      # auto-detect from TM## in filenames
    corrected_suffix=".corrected",          # output folder suffix
    specimen=0,                             # default specimen index
    camera_pairs=[(0, 1), (2, 3)],          # ortho camera pairs to fuse

    # output
    output_format="tif",                    # tif, zarr, or klb
    compression="zstd",                     # zstd, lzw, deflate, or None
    compression_level=3,                    # 1-22 for zstd, 1-9 for others

    # transforms (applied to second camera in each pair)
    rotation=0,                             # 0=none, 1=90cw, -1=90ccw
    flip_horizontal=False,
    flip_vertical=False,

    # correction
    median_kernel=(3, 3),                   # dead pixel filter, None to disable
    background_percentile=5.0,
    mask_percentile=1.0,
    segment_mode=1,                         # 0=none, 1=segment+mask, 2=masks, 3=global

    # fusion
    blending_method="adaptive",             # adaptive, geometric, average, wavelet
    blending_range=20,                      # transition zone width (z-planes)

    # per-specimen overrides (tiled mode)
    # tile_crops={"SPM00": {"crop_depth": {0: 450}}}
    # view_orientation={"SPM00": {"flip_axis": 1}, "SPM01": {"flip_axis": 0}}
)

correct_stack(config)
multi_fuse(config)
```
