|
| 1 | +<h1 align="center">scigantic-bil</h1> |
| 2 | + |
| 3 | +<p align="center"> |
| 4 | + <a href="https://github.com/Scigantic/scigantic-bil/actions/workflows/ci.yml"> |
| 5 | + <img alt="CI" src="https://github.com/Scigantic/scigantic-bil/actions/workflows/ci.yml/badge.svg" /></a> |
| 6 | + <a href="https://pypi.org/project/scigantic-bil/"> |
| 7 | + <img alt="PyPI" src="https://img.shields.io/pypi/v/scigantic-bil" /></a> |
| 8 | + <a href="https://pypi.org/project/scigantic-bil/"> |
| 9 | + <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/scigantic-bil" /></a> |
| 10 | + <a href="https://github.com/Scigantic/scigantic-bil/blob/main/LICENSE"> |
| 11 | + <img alt="License" src="https://img.shields.io/github/license/Scigantic/scigantic-bil" /></a> |
| 12 | +</p> |
| 13 | + |
| 14 | +Search the [Brain Image Library](https://www.brainimagelibrary.org/) and read its light-sheet, fMOST and STPT volumes over HTTP. No download, no account, no local copy. |
| 15 | + |
| 16 | +```python |
| 17 | +import scigantic_bil as bil |
| 18 | + |
| 19 | +cat = bil.BilCatalog.load() # every BIL dataset, one 5 MB GET |
| 20 | +for d in cat.light_sheet()[:5]: # 808 light-sheet datasets, 278 TB |
| 21 | + print(d.bildid, d.contributor, d.species, f"{d.size_gb:.0f} GB") |
| 22 | + |
| 23 | +img = bil.thumbnail("ace-bin-run") # middle z-slice of a 27 GB stack, one 16 MB read |
| 24 | +``` |
| 25 | + |
| 26 | +## Installation |
| 27 | + |
| 28 | +```console |
| 29 | +$ pip install scigantic-bil |
| 30 | +$ pip install "scigantic-bil[zarr]" # also open OME-Zarr stores in place |
| 31 | +``` |
| 32 | + |
| 33 | +## Why this exists |
| 34 | + |
| 35 | +The Brain Image Library is the BRAIN Initiative's repository for whole-brain microscopy: 14,224 datasets and 6 PB as of its 2026-07-31 inventory, including the largest public collection of cleared-tissue light-sheet brains. Everything is served over plain HTTPS with directory listings and range requests, and the metadata API needs no key. That makes it readable in place, but nothing in the ecosystem did so. |
| 36 | + |
| 37 | +BIL's own [`brainimagelibrary`](https://pypi.org/project/brainimagelibrary/) package (py-brain-sdk, 0.0.23, GPL-3.0) wraps the metadata API and downloads datasets, with resumable transfers and citation lookups. Its source (read, not assumed, on 2026-09-08) contains no image reading at all: the only route from a BIL id to pixels is `DatasetInventory.download()`, which fetches the files to disk. For a 27 GB light-sheet stack that is the whole stack, to look at one slice. |
| 38 | + |
| 39 | +This package is the other half. It reads BIL in place: |
| 40 | + |
| 41 | +- **A typed, in-memory catalog of the whole archive** from BIL's daily inventory TSV, with structured filters and a light-sheet finder that unions the inventory's technique field with BIL's fulltext index. The technique field alone says light sheet for 348 datasets; the union finds 808. The rest are tagged `other` and only say light sheet in their abstract or instrument record. |
| 42 | +- **Single-slice TIFF reads** straight from the download server. The dominant BIL layout is one TIFF per z-plane, about 16 MB each; one slice is one request. |
| 43 | +- **OME-Zarr stores** opened lazily through zarr's HTTP store, with pyramid levels checked against what the server actually has (one BIL store declares eight levels and serves seven). |
| 44 | +- **Thumbnails that read as little as possible**: the middle slice of a TIFF stack, or the coarsest level of a zarr pyramid. |
| 45 | +- **A seekable HTTP file object** (`HttpFile`) so tifffile fetches only the pages you ask for from a large multi-page TIFF. |
| 46 | +- **BIL's per-dataset manifest** (path, size, MD5, URL for every file) as one gzipped GET, so a deep tree lists in one request. |
| 47 | + |
| 48 | +Measured on 2026-09-08 from a residential connection, against the live archive: |
| 49 | + |
| 50 | +| | measured | |
| 51 | +|---|---| |
| 52 | +| Load the full catalog (14,224 datasets) | 1.0 s cold, 0.3 s from the on-disk copy | |
| 53 | +| Light-sheet datasets found | 348 by technique field, 808 with fulltext union | |
| 54 | +| Read one 16 MB light-sheet slice (4501 x 3828 uint16) | 0.40 s, 45 MB/s single stream | |
| 55 | +| Thumbnail of a 1,923-slice, 27 GB stack | 0.25 s to 0.56 s, one file read | |
| 56 | +| Thumbnail of a 50 GB OME-Zarr store (848 x 6300 x 9600) | 0.65 s, coarsest level only | |
| 57 | +| Eight parallel streams | 19 MB/s aggregate, slower than one stream, so reads are sequential | |
| 58 | +| Manifest for a 1,923-file dataset | 352 KB gzipped, one request | |
| 59 | + |
| 60 | +Dependencies are `requests`, `numpy` and `tifffile`. zarr and pandas are extras. |
| 61 | + |
| 62 | +## Data license |
| 63 | + |
| 64 | +BIL data is distributed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/); some datasets additionally carry the Allen Institute Terms of Use, recorded per dataset in `DatasetDetail.rights`. This package's code is MIT-0. The permissive code license does not extend to the data: anything you derive from BIL images and redistribute needs attribution and the same license. Cite the dataset's DOI (`DatasetDetail.doi`) and its publications (`DatasetDetail.publications`). |
| 65 | + |
| 66 | +## Catalog |
| 67 | + |
| 68 | +```python |
| 69 | +cat = bil.BilCatalog.load() # newest daily inventory |
| 70 | +cat = bil.BilCatalog.load(date="20260731") # a specific snapshot |
| 71 | +len(cat), cat.date # (14224, '20260731') |
| 72 | + |
| 73 | +cat.summary() # datasets, files, TB, top modality/technique/species/extensions |
| 74 | +cat["ace-cup-eel"] # one Dataset by id |
| 75 | +cat.filter(technique="fMOST", species="mouse") # case-insensitive substring match, any combination |
| 76 | +cat.filter(extension=".swc") # datasets shipping neuron reconstructions |
| 77 | +cat.filter(max_size_gb=2) # small enough to pull whole |
| 78 | +cat.search("iDISCO") # BIL's fulltext index, joined to inventory rows |
| 79 | +cat.light_sheet() # technique field + fulltext, deduplicated |
| 80 | +cat.to_dataframe(cat.filter(technique="STPT")) # pandas, with pip install "scigantic-bil[pandas]" |
| 81 | +``` |
| 82 | + |
| 83 | +A `Dataset` carries what the inventory indexes: contributor, affiliation, award, project, consortium, modality, technique, species, genotype, file count, size, and a per-extension file histogram (`extensions`). `Dataset.url` is the dataset's root on the download server. |
| 84 | + |
| 85 | +## Metadata |
| 86 | + |
| 87 | +The full record lives on the metadata API and is fetched on demand: |
| 88 | + |
| 89 | +```python |
| 90 | +d = bil.retrieve("ace-cup-eel") |
| 91 | +d.title, d.abstract, d.rights_identifier # 'Light-sheet imaged brain ...', ..., 'CC-BY-SA-4.0' |
| 92 | +d.microscope_type, d.species # 'Light-sheet', 'mouse' |
| 93 | +d.instrument # {'microscopetype': 'Light-sheet', 'microscopemanufacturerandmodel': 'Zeiss Z.1', ...} |
| 94 | +d.specimen, d.images # specimen record; per-image axes, step sizes, channels |
| 95 | +d.contributors, d.publications, d.funders |
| 96 | +d.is_light_sheet # checks technique, instrument and free text together |
| 97 | + |
| 98 | +bil.retrieve_many(["ace-cup-eel", "ace-bin-run"]) # batched POST, unknown ids dropped |
| 99 | +bil.fulltext("CLARITY") # BIL ids only |
| 100 | +bil.query("specimen", species="mouse") # one structured element=value pair |
| 101 | +``` |
| 102 | + |
| 103 | +Structured queries match exactly (`query("instrument", microscopetype="Light-sheet")` found 6 datasets on 2026-09-08 where `fulltext("light sheet")` found 777), so use fulltext for discovery and the catalog's filters for structure. |
| 104 | + |
| 105 | +## Files |
| 106 | + |
| 107 | +```python |
| 108 | +bil.list_files("ace-bin-run") # one directory: name, size, modified, url |
| 109 | +bil.manifest("ace-bin-run") # every file, with relative path and MD5, one gzipped GET |
| 110 | +list(bil.walk("ace-cup-eel")) # recursive; a zarr store appears once, as a directory |
| 111 | +bil.find("ace-bin-run", suffix=".tif") # natural sort, so Z00002 follows Z00001 |
| 112 | +bil.find_zarr("ace-cup-eel") # ['https://download.brainimagelibrary.org/.../subject_5.zarr/'] |
| 113 | +bil.download(entry, "out/") # the one function that writes image bytes to disk |
| 114 | +``` |
| 115 | + |
| 116 | +Any of these accept a BIL id, a `Dataset`, a `/bil/data/...` path from the metadata, or a download-server URL. |
| 117 | + |
| 118 | +## Images |
| 119 | + |
| 120 | +```python |
| 121 | +stack = bil.slices("ace-bin-run") # 1,923 FileEntry in z order, nothing fetched yet |
| 122 | +img = bil.read_tiff(stack[961]) # (4501, 3828) uint16, one request |
| 123 | +vol = bil.read_stack("ace-bin-run", start=900, stop=960, step=10) # (6, 4501, 3828) |
| 124 | +bil.thumbnail("ace-bin-run", max_size=512) # middle slice, stride-downsampled |
| 125 | +bil.thumbnail("ace-bin-run", index=100, channel="ch02") |
| 126 | +``` |
| 127 | + |
| 128 | +Formats this package does not decode raise `UnsupportedFormatError` naming what does: JPEG 2000 (`.jp2`, 5,787 datasets, mostly STPT and fMOST sections; use `download()` then glymur), Imaris (`.ims`, h5py), NIfTI. Reading `.jp2` in place is the obvious next addition; the format has resolution levels built in, so a thumbnail should not need the whole file. |
| 129 | + |
| 130 | +### OME-Zarr |
| 131 | + |
| 132 | +```python |
| 133 | +g = bil.open_zarr("ace-cup-eel") # lazy; nothing read until sliced |
| 134 | +bil.zarr_levels(g) # ['0', ..., '6'], as served, not as declared |
| 135 | +g["6"][0, 0, 400] # one plane of the coarsest level, a few chunks |
| 136 | +bil.zarr_thumbnail(g, max_size=512) |
| 137 | +``` |
| 138 | + |
| 139 | +A zarr store is a directory, so it never shows in the inventory's extension histogram; `find_zarr()` is how to know a dataset ships one. Requires `pip install "scigantic-bil[zarr]"` (zarr 3, fsspec, aiohttp). |
| 140 | + |
| 141 | +## Caching |
| 142 | + |
| 143 | +On by default. Metadata responses, directory listings and manifests are cached to `~/.cache/scigantic-bil` (macOS: `~/Library/Caches/scigantic-bil`; override with `SCIGANTIC_BIL_CACHE` or `enable_cache(cache_dir=...)`) and expire after 7 days, since BIL republishes its inventory every few days. Inventory snapshots are immutable once published and are kept as plain TSV files without expiry. Image bytes are never cached. |
| 144 | + |
| 145 | +```python |
| 146 | +bil.disable_cache() |
| 147 | +bil.enable_cache(ttl_days=1) |
| 148 | +bil.clear_cache() |
| 149 | +``` |
| 150 | + |
| 151 | +## Command line |
| 152 | + |
| 153 | +```console |
| 154 | +$ scigantic-bil summary |
| 155 | +$ scigantic-bil light-sheet --limit 20 |
| 156 | +$ scigantic-bil search "iDISCO" --json |
| 157 | +$ scigantic-bil filter --technique fMOST --species mouse --max-gb 100 |
| 158 | +$ scigantic-bil info ace-cup-eel |
| 159 | +$ scigantic-bil files ace-bin-run --zarr |
| 160 | +$ scigantic-bil thumbnail ace-bin-run slice.png --size 512 |
| 161 | +``` |
| 162 | + |
| 163 | +## Testing |
| 164 | + |
| 165 | +Every test runs live against BIL, no mocks, the same philosophy as the rest of the scigantic-* packages. The suite takes about 15 seconds. CI runs Python 3.10 through 3.14 plus `mypy --strict`. |
| 166 | + |
| 167 | +## License |
| 168 | + |
| 169 | +MIT-0 for the code. See [Data license](#data-license) for the data. |
0 commit comments