xenium-splitter 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. xenium_splitter-0.2.1/.github/workflows/publish.yml +129 -0
  2. xenium_splitter-0.2.1/.gitignore +23 -0
  3. xenium_splitter-0.2.1/LICENSE +21 -0
  4. xenium_splitter-0.2.1/PKG-INFO +419 -0
  5. xenium_splitter-0.2.1/README.md +389 -0
  6. xenium_splitter-0.2.1/TODO.md +153 -0
  7. xenium_splitter-0.2.1/WORKFLOW_README.md +293 -0
  8. xenium_splitter-0.2.1/pyproject.toml +58 -0
  9. xenium_splitter-0.2.1/release.md +46 -0
  10. xenium_splitter-0.2.1/scripts/BENCHMARK_README.md +360 -0
  11. xenium_splitter-0.2.1/scripts/benchmark_capture.py +137 -0
  12. xenium_splitter-0.2.1/scripts/benchmark_config_example.csv +15 -0
  13. xenium_splitter-0.2.1/scripts/benchmark_example_output.txt +2988 -0
  14. xenium_splitter-0.2.1/scripts/benchmark_report.py +1316 -0
  15. xenium_splitter-0.2.1/scripts/benchmark_submit.py +553 -0
  16. xenium_splitter-0.2.1/setup.cfg +4 -0
  17. xenium_splitter-0.2.1/src/xenium_splitter/__init__.py +6 -0
  18. xenium_splitter-0.2.1/src/xenium_splitter/cli.py +237 -0
  19. xenium_splitter-0.2.1/src/xenium_splitter/estimator.py +20 -0
  20. xenium_splitter-0.2.1/src/xenium_splitter/image_utils.py +1104 -0
  21. xenium_splitter-0.2.1/src/xenium_splitter/io_utils.py +3720 -0
  22. xenium_splitter-0.2.1/src/xenium_splitter/lasso.py +244 -0
  23. xenium_splitter-0.2.1/src/xenium_splitter/metadata.py +449 -0
  24. xenium_splitter-0.2.1/src/xenium_splitter/models.py +87 -0
  25. xenium_splitter-0.2.1/src/xenium_splitter/recalculate_diffexp.py +366 -0
  26. xenium_splitter-0.2.1/src/xenium_splitter/show_regions.py +253 -0
  27. xenium_splitter-0.2.1/src/xenium_splitter/splitter.py +3032 -0
  28. xenium_splitter-0.2.1/src/xenium_splitter/validate_transcripts_zarr.py +43 -0
  29. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/PKG-INFO +419 -0
  30. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/SOURCES.txt +41 -0
  31. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/dependency_links.txt +1 -0
  32. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/entry_points.txt +4 -0
  33. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/requires.txt +24 -0
  34. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/scm_file_list.json +37 -0
  35. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/scm_version.json +8 -0
  36. xenium_splitter-0.2.1/src/xenium_splitter.egg-info/top_level.txt +1 -0
  37. xenium_splitter-0.2.1/tests/test_benchmark_capture.py +116 -0
  38. xenium_splitter-0.2.1/tests/test_benchmark_report.py +240 -0
  39. xenium_splitter-0.2.1/tests/test_benchmark_submit.py +183 -0
  40. xenium_splitter-0.2.1/tests/test_estimator.py +72 -0
  41. xenium_splitter-0.2.1/tests/test_image_utils.py +84 -0
  42. xenium_splitter-0.2.1/tests/test_lasso.py +37 -0
  43. xenium_splitter-0.2.1/tests/test_show_regions.py +94 -0
@@ -0,0 +1,129 @@
1
+ name: Publish To PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: pypi-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ build:
17
+ name: Test And Build
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 20
20
+ env:
21
+ RELEASE_TAG: ${{ github.ref_name }}
22
+ steps:
23
+ - name: Check Out Release Tag
24
+ uses: actions/checkout@v4
25
+ with:
26
+ fetch-depth: 0
27
+ persist-credentials: false
28
+
29
+ - name: Set Up Python
30
+ uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.12"
33
+
34
+ - name: Validate Release Tag
35
+ run: |
36
+ python -m pip install packaging
37
+ python - <<'PY'
38
+ import os
39
+ from packaging.version import Version
40
+
41
+ version = Version(os.environ["RELEASE_TAG"].removeprefix("v"))
42
+ if version.local is not None or version.is_devrelease:
43
+ raise SystemExit("Release tags must not contain local or development versions")
44
+ print(f"Release version: {version}")
45
+ PY
46
+
47
+ - name: Install Build And Test Dependencies
48
+ run: |
49
+ python -m pip install --upgrade pip
50
+ python -m pip install -e ".[dev]" build twine
51
+
52
+ - name: Run Tests
53
+ run: python -m pytest
54
+
55
+ - name: Build Distributions
56
+ run: python -m build
57
+
58
+ - name: Check Distribution Metadata And Versions
59
+ run: |
60
+ python -m twine check dist/*
61
+ python - <<'PY'
62
+ import os
63
+ import tarfile
64
+ import zipfile
65
+ from email.parser import BytesParser
66
+ from pathlib import Path
67
+ from packaging.version import Version
68
+
69
+ expected = Version(os.environ["RELEASE_TAG"].removeprefix("v"))
70
+ wheels = list(Path("dist").glob("*.whl"))
71
+ sources = list(Path("dist").glob("*.tar.gz"))
72
+ if len(wheels) != 1 or len(sources) != 1:
73
+ raise SystemExit("Expected exactly one wheel and one source distribution")
74
+ with zipfile.ZipFile(wheels[0]) as archive:
75
+ metadata_path = next(
76
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
77
+ )
78
+ wheel_metadata = BytesParser().parsebytes(archive.read(metadata_path))
79
+ with tarfile.open(sources[0]) as archive:
80
+ metadata_member = next(
81
+ member for member in archive.getmembers()
82
+ if member.name.count("/") == 1 and member.name.endswith("/PKG-INFO")
83
+ )
84
+ source_metadata = BytesParser().parsebytes(
85
+ archive.extractfile(metadata_member).read()
86
+ )
87
+ for metadata in (wheel_metadata, source_metadata):
88
+ actual = Version(metadata["Version"])
89
+ if actual != expected:
90
+ raise SystemExit(f"Tag version {expected} does not match build version {actual}")
91
+ print(f"Both distributions match release tag: {expected}")
92
+ PY
93
+
94
+ - name: Smoke Test Installed Wheel
95
+ working-directory: ${{ runner.temp }}
96
+ run: |
97
+ python -m venv "$RUNNER_TEMP/wheel-smoke"
98
+ "$RUNNER_TEMP/wheel-smoke/bin/python" -m pip install "$GITHUB_WORKSPACE"/dist/*.whl
99
+ "$RUNNER_TEMP/wheel-smoke/bin/python" -m pip check
100
+ "$RUNNER_TEMP/wheel-smoke/bin/xenium-splitter" --help
101
+ "$RUNNER_TEMP/wheel-smoke/bin/xenium-recalc-diffexp" --help
102
+ "$RUNNER_TEMP/wheel-smoke/bin/xenium-validate-transcripts-zarr" --help
103
+
104
+ - name: Upload Release Distributions
105
+ uses: actions/upload-artifact@v4
106
+ with:
107
+ name: distributions
108
+ path: dist/*
109
+ if-no-files-found: error
110
+
111
+ publish:
112
+ name: Publish To PyPI
113
+ needs: build
114
+ runs-on: ubuntu-latest
115
+ timeout-minutes: 10
116
+ environment:
117
+ name: pypi
118
+ url: https://pypi.org/project/xenium-splitter/
119
+ permissions:
120
+ id-token: write
121
+ steps:
122
+ - name: Download Release Distributions
123
+ uses: actions/download-artifact@v4
124
+ with:
125
+ name: distributions
126
+ path: dist
127
+
128
+ - name: Publish With Trusted Publishing
129
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+ dist/
12
+ build/
13
+ *.egg-info/
14
+ .venv/
15
+ venv/
16
+
17
+ # local outputs
18
+ output/
19
+
20
+ # large raw data
21
+ data/
22
+
23
+ data_out/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,419 @@
1
+ Metadata-Version: 2.4
2
+ Name: xenium-splitter
3
+ Version: 0.2.1
4
+ Summary: Split Xenium outputs by LASSO regions and optionally process matching H&E images.
5
+ Author: xenium-splitter contributors
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy<2,>=1.26
11
+ Requires-Dist: opencv-python-headless<4.12,>=4.8
12
+ Requires-Dist: pandas>=2.2
13
+ Requires-Dist: pyarrow>=15.0
14
+ Requires-Dist: h5py>=3.10
15
+ Requires-Dist: zarr<=3.0.0
16
+ Requires-Dist: Pillow>=10.0
17
+ Requires-Dist: shapely>=2.0
18
+ Requires-Dist: tifffile>=2024.5.22
19
+ Requires-Dist: imagecodecs>=2024.1.1
20
+ Requires-Dist: typer>=0.12
21
+ Provides-Extra: parquet
22
+ Provides-Extra: svs
23
+ Requires-Dist: openslide-python>=1.3.1; extra == "svs"
24
+ Provides-Extra: hdf5
25
+ Provides-Extra: zarr
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # xenium-splitter
32
+
33
+ `xenium-splitter` splits Xenium output files by user-defined LASSO regions and writes one output
34
+ directory per region. It can also process a matching H&E image, applying polygon masks and
35
+ cropping to each region.
36
+
37
+ ## Features
38
+
39
+ - CLI-first workflow.
40
+ - LASSO region parsing from GeoJSON/JSON or CSV/TSV.
41
+ - Tabular Xenium split (CSV/TSV/TXT/Parquet) using detected coordinate columns.
42
+ - Coordinate re-basing in each region output (region origin becomes `(0, 0)`).
43
+ - Image masking + cropping for raster images (`.png`, `.jpg`, `.jpeg`, `.tif`, `.tiff`, `.ome.tif`, `.ome.tiff`).
44
+ - Optional SVS handling for H&E: load, split, and optionally convert to OME-TIFF.
45
+ - `show-regions` preview command: render labeled LASSO regions and dotted region bounding boxes on low-res H&E/morphology images.
46
+ - Metadata README generation with run parameters and file-level metrics.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install -e .
52
+ ```
53
+
54
+ Zarr, HDF5, and Parquet support are included in the default installation.
55
+
56
+ Optional extras:
57
+
58
+ ```bash
59
+ pip install -e ".[svs,dev]"
60
+ ```
61
+
62
+ The `svs` extra adds SVS image support and requires native OpenSlide libraries.
63
+ The `dev` extra adds testing and linting tools. The legacy `parquet`, `hdf5`, and
64
+ `zarr` extras remain accepted for compatibility but are no longer necessary.
65
+
66
+ For versioning and publishing instructions, see [release.md](release.md).
67
+
68
+ ## CLI Usage
69
+
70
+ ### RAM Estimate
71
+
72
+ ```bash
73
+ xenium-splitter estimate-ram \
74
+ --input-dir /path/to/xenium_output \
75
+ --lasso-file regions.csv \
76
+ --output-dir /path/to/output
77
+ ```
78
+
79
+ Estimates peak RAM using the on-disk archive sizes directly inside `--input-dir`:
80
+
81
+ ```text
82
+ PeakRAM(GB) = 2.5 + 0.010 * TranscriptZarrMB + 0.007 * CellZarrMB
83
+ ```
84
+
85
+ `TranscriptZarrMB` is the size of `transcripts.zarr.zip`; `CellZarrMB` is the size
86
+ of `cells.zarr.zip`. Both files are required. MB means 1,000,000 bytes, not MiB.
87
+ The estimator reads file sizes only; it does not load or extract the archives.
88
+ It accepts all the same options, defaults, and required paths as `split`; replace
89
+ `split` with `estimate-ram` in a split invocation. This includes repeated
90
+ `--include-glob` options, image/analysis switches, and `-v`/`--verbose`.
91
+ `--images-only` and `--skip-images` remain mutually exclusive.
92
+ Only the two archive sizes affect this equation: processing options, LASSO
93
+ geometry, H&E, and the output path are not modeled. No output files or directories
94
+ are created.
95
+
96
+ This is an approximate peak estimate, not a guaranteed memory limit or a scheduler
97
+ allocation recommendation. Image layout, region geometry, and processing options
98
+ are not modeled; allow additional headroom when requesting RAM.
99
+
100
+ ### Options
101
+
102
+ **Required:**
103
+
104
+ | Option | Description |
105
+ | -------------- | -------------------------------------------------------------------------------------------------------- |
106
+ | `--input-dir` | Xenium output directory (the `outs/` folder or equivalent). |
107
+ | `--lasso-file` | LASSO region file — GeoJSON/JSON or CSV/TSV (see [LASSO Input Expectations](#lasso-input-expectations)). |
108
+ | `--output-dir` | Destination directory; one `region_<id>/` subfolder is created per region. |
109
+
110
+ **Optional — H&E image:**
111
+
112
+ | Option | Default | Description |
113
+ | ---------------------- | ------- | -------------------------------------------------------------------------------------- |
114
+ | `--he-image` | None | External H&E image to split alongside Xenium outputs (TIFF, OME-TIFF, SVS, PNG, JPEG). |
115
+ | `--convert-svs-to-ome` | off | When `--he-image` is an SVS file, also write a full-slide OME-TIFF per region. |
116
+
117
+ > **Note:** xenium-splitter does **not** perform H&E registration. The H&E image is assumed to already be spatially aligned with the Xenium coordinate space. No registration file, affine transform, or warping is applied. If your H&E requires registration to the morphology image, that step must be completed before running this tool. The tool also assumes the H&E image uses the **same pixel size** as the Xenium morphology data — it reads `pixel_size_um` from `experiment.xenium` and uses that value to convert polygon coordinates to pixel coordinates for both. If your H&E has a different pixel resolution, the crop will be incorrectly positioned.
118
+
119
+ **Optional — image processing:**
120
+
121
+ | Option | Default | Description |
122
+ | ---------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123
+ | `--squash-layers` / `--no-squash-layers` | `--squash-layers` | Flatten multi-layer TIFF stacks to 2D/RGB before cropping. |
124
+ | `--skip-images` / `--process-images` | `--process-images` | Skip all image cropping and masking (useful for fast data-only runs). |
125
+ | `--images-only` | off | Process **only** image files; skip all tabular data, zarr, CFM, and diffexp stages. Useful when running images separately to keep RAM available, or for a re-run after a prior data-only pass. Mutually exclusive with `--skip-images`. |
126
+ | `--overlays` / `--no-overlays` | `--no-overlays` | Write annotated FOV grid overlay images alongside each region `morphology_mip.ome.tif`. |
127
+
128
+ **Optional — data filtering:**
129
+
130
+ | Option | Default | Description |
131
+ | -------------------- | ---------------- | ---------------------------------------------------------------------------------- |
132
+ | `--include-glob` | None (all files) | Glob pattern to restrict which files are read from `--input-dir`; repeatable. |
133
+ | `--copy-transcripts` | off | Copy `transcripts.zarr.zip` verbatim to each region instead of filtering/rebasing. |
134
+
135
+ **Optional — analysis outputs:**
136
+
137
+ | Option | Default | Description |
138
+ | ---------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------- |
139
+ | `--recalculate-diffexp` / `--skip-diffexp-recalc` | `--recalculate-diffexp` | Recompute `analysis/diffexp` CSVs from the filtered matrix and clustering labels. |
140
+ | `--write-cell-feature-matrix-zarr` / `--skip-cell-feature-matrix-zarr` | `--write-cell-feature-matrix-zarr` | Write `cell_feature_matrix.zarr.zip` in each region output. |
141
+
142
+ **Optional — logging:**
143
+
144
+ | Option | Default | Description |
145
+ | ------------------ | ------- | ------------------------------------- |
146
+ | `-v` / `--verbose` | off | Enable debug-level logging to stderr. |
147
+
148
+ ### Examples
149
+
150
+ **Minimal split — tabular data and morphology only:**
151
+
152
+ ```bash
153
+ xenium-splitter split \
154
+ --input-dir /path/to/xenium_output \
155
+ --lasso-file regions.geojson \
156
+ --output-dir /path/to/output
157
+ ```
158
+
159
+ **Fast data-only run — skip all image processing:**
160
+
161
+ ```bash
162
+ xenium-splitter split \
163
+ --input-dir /path/to/xenium_output \
164
+ --lasso-file regions.csv \
165
+ --output-dir /path/to/output \
166
+ --skip-images -v
167
+ ```
168
+
169
+ **Include a pre-aligned H&E TIFF alongside Xenium outputs:**
170
+
171
+ ```bash
172
+ xenium-splitter split \
173
+ --input-dir /path/to/xenium_output \
174
+ --lasso-file regions.geojson \
175
+ --output-dir /path/to/output \
176
+ --he-image /path/to/aligned_he.ome.tif
177
+ ```
178
+
179
+ **Include an SVS H&E and also produce per-region OME-TIFF exports:**
180
+
181
+ ```bash
182
+ xenium-splitter split \
183
+ --input-dir /path/to/xenium_output \
184
+ --lasso-file regions.geojson \
185
+ --output-dir /path/to/output \
186
+ --he-image /path/to/he.svs \
187
+ --convert-svs-to-ome
188
+ ```
189
+
190
+ **Write FOV grid overlays for visual QC of region alignment:**
191
+
192
+ ```bash
193
+ xenium-splitter split \
194
+ --input-dir /path/to/xenium_output \
195
+ --lasso-file regions.geojson \
196
+ --output-dir /path/to/output \
197
+ --overlays
198
+ ```
199
+
200
+ **Process only a subset of files matching a glob pattern:**
201
+
202
+ ```bash
203
+ xenium-splitter split \
204
+ --input-dir /path/to/xenium_output \
205
+ --lasso-file regions.geojson \
206
+ --output-dir /path/to/output \
207
+ --include-glob "cells*" \
208
+ --include-glob "transcripts*" \
209
+ --skip-images
210
+ ```
211
+
212
+ **Copy transcripts verbatim (no filtering or coordinate rebasing):**
213
+
214
+ ```bash
215
+ xenium-splitter split \
216
+ --input-dir /path/to/xenium_output \
217
+ --lasso-file regions.geojson \
218
+ --output-dir /path/to/output \
219
+ --copy-transcripts
220
+ ```
221
+
222
+ **Images-only re-run after a prior data-only pass (minimise peak RAM):**
223
+
224
+ ```bash
225
+ # Step 1 — data only (low RAM):
226
+ xenium-splitter split \
227
+ --input-dir /path/to/xenium_output \
228
+ --lasso-file regions.geojson \
229
+ --output-dir /path/to/output \
230
+ --skip-images
231
+
232
+ # Step 2 — images only (reserves all RAM for image loading):
233
+ xenium-splitter split \
234
+ --input-dir /path/to/xenium_output \
235
+ --lasso-file regions.geojson \
236
+ --output-dir /path/to/output \
237
+ --images-only
238
+ ```
239
+
240
+ **Images-only with a large SVS H&E:**
241
+
242
+ ```bash
243
+ xenium-splitter split \
244
+ --input-dir /path/to/xenium_output \
245
+ --lasso-file regions.geojson \
246
+ --output-dir /path/to/output \
247
+ --images-only \
248
+ --he-image /path/to/he.svs
249
+ ```
250
+
251
+ ### Temp Cleanup
252
+
253
+ Remove temporary working directories created under the OS temp folder
254
+ (`.../xenium_splitter`):
255
+
256
+ ```bash
257
+ xenium-splitter clean-temp
258
+ ```
259
+
260
+ ### Region Preview (show-regions)
261
+
262
+ Create a quick visual QC image (max `2000 x 2000`) that overlays LASSO regions and
263
+ region bounding boxes on a source tissue image.
264
+
265
+ Behavior:
266
+
267
+ - Source image preference: H&E first, morphology fallback.
268
+ - Region outline color: purple.
269
+ - Dotted region bounding box color:
270
+ - H&E source: black.
271
+ - Morphology source: green.
272
+ - Region labels: each `region_id` label is drawn once per bounding box in the top-inside corner with a small margin.
273
+ - Label font size scales with generated image size for readability.
274
+
275
+ CLI options:
276
+
277
+ | Option | Description |
278
+ | -------------------- | ------------------------------------------------------------------------------ |
279
+ | `--input-dir` | Xenium input directory used to discover source images and `experiment.xenium`. |
280
+ | `--lasso-file` | LASSO region file (GeoJSON/JSON or CSV/TSV). |
281
+ | `--output-image` | Output preview image path (for example `regions_preview.png`). |
282
+ | `--he-image` | Optional explicit H&E image path; when provided, this is used directly. |
283
+ | `--max-dimension-px` | Longest output edge in pixels (max allowed value is `2000`, default `2000`). |
284
+
285
+ Examples:
286
+
287
+ ```bash
288
+ xenium-splitter show-regions \
289
+ --input-dir /path/to/xenium_output \
290
+ --lasso-file regions.geojson \
291
+ --output-image /path/to/output/regions_preview.png
292
+ ```
293
+
294
+ ```bash
295
+ xenium-splitter show-regions \
296
+ --input-dir /path/to/xenium_output \
297
+ --lasso-file regions.csv \
298
+ --he-image /path/to/aligned_he.svs \
299
+ --output-image /path/to/output/regions_preview.png
300
+ ```
301
+
302
+ ## LASSO Input Expectations
303
+
304
+ ### GeoJSON / JSON
305
+
306
+ - `FeatureCollection` with polygon-like geometries (`Polygon` or `MultiPolygon`).
307
+ - Region id is read from `id`, `region_id`, `name`, or generated automatically.
308
+
309
+ ### CSV / TSV
310
+
311
+ Two supported layouts:
312
+
313
+ 1. Point list with columns: `region_id`, `x`, `y`.
314
+ 2. WKT with columns: `region_id`, `polygon_wkt`.
315
+
316
+ ## Output Layout
317
+
318
+ For each region, a directory is created:
319
+
320
+ ```text
321
+ output/
322
+ region_<id>/
323
+ ...split files mirroring input names...
324
+ run_metadata_README.md
325
+ ```
326
+
327
+ ## RAM Requirements
328
+
329
+ Memory usage is driven by three independent factors: transcript table size, image size, and zarr processing. They do not all peak simultaneously, but on large datasets they can overlap significantly. Note, official benchmarking has not been done on this yet. Once benchmarks are available, they will be added to this section.
330
+
331
+ ### Without image processing (`--skip-images`)
332
+
333
+ The dominant cost is the full transcript table, which is loaded entirely into RAM as a pandas DataFrame before being filtered per region.
334
+
335
+ | Dataset scale | Approx. transcripts | Recommended RAM |
336
+ | ---------------- | ------------------- | --------------- |
337
+ | Small (< 1 mm²) | ~500 K | 4 GB |
338
+ | Medium (~10 mm²) | ~5 M | 8 GB |
339
+ | Large (~50 mm²) | ~50 M | 16–32 GB |
340
+
341
+ The cell/barcode tables and CFM zarr add a modest additional cost (typically < 1 GB).
342
+
343
+ ### With image processing (default)
344
+
345
+ Image memory cost depends on format and adds on top of the transcript cost above.
346
+
347
+ **Morphology TIFF / OME-TIFF — always full-image load:**
348
+ The entire morphology image is read into RAM before cropping; windowed reads are not used for TIFF. A temporary copy is created during Z-stack squashing (max-projection), so peak is approximately 2× the raw array size.
349
+
350
+ | Image dimensions | Channels / planes | RAM (peak ~2×) |
351
+ | ---------------- | ------------------ | -------------- |
352
+ | 5 K × 5 K | 1 plane, uint16 | ~100 MB |
353
+ | 10 K × 10 K | 1 plane, uint16 | ~400 MB |
354
+ | 20 K × 20 K | 7 Z-planes, uint16 | ~11 GB |
355
+ | 30 K × 30 K | 7 Z-planes, uint16 | ~25 GB |
356
+
357
+ **H&E as TIFF / OME-TIFF — full-image load:**
358
+ Same behaviour as morphology: the complete image is loaded before any cropping.
359
+
360
+ | Image dimensions | Channels | RAM (peak ~2×) |
361
+ | ---------------- | --------- | -------------- |
362
+ | 10 K × 20 K | RGB uint8 | ~1.2 GB |
363
+ | 25 K × 45 K | RGB uint8 | ~6.8 GB |
364
+
365
+ **H&E as SVS — windowed, memory-efficient:**
366
+ OpenSlide reads only the polygon bounding box per region; the full slide is never loaded. A typical crop (5 K × 5 K px) uses ~100 MB regardless of source file size. **SVS is the recommended format for large H&E images.**
367
+
368
+ ### Practical guidance
369
+
370
+ | Scenario | Recommended minimum RAM |
371
+ | --------------------------------------------- | ----------------------- |
372
+ | Data only, small dataset | 8 GB |
373
+ | Data only, large dataset (≥ 50 M transcripts) | 32–64 GB |
374
+ | Data + morphology TIFF, medium dataset | 16 GB |
375
+ | Data + H&E TIFF (25 K × 45 K), medium dataset | 32 GB |
376
+ | Data + H&E TIFF (25 K × 45 K), large dataset | 64 GB |
377
+ | Data + H&E SVS (any size), medium dataset | 16 GB |
378
+ | Data + H&E SVS (any size), large dataset | 32–48 GB |
379
+
380
+ > **Tip:** Run `--skip-images` first for a fast data-only pass. Re-run with images only if needed. For large H&E files, convert to SVS or use a tiled OME-TIFF with a viewer that supports windowed reads before passing to this tool.
381
+
382
+ ## Current Scope and Notes
383
+
384
+ - Files without recognized coordinate columns (for tabular) are skipped and documented in metadata.
385
+ - Unknown binary formats are currently skipped.
386
+ - SVS requires `openslide-python` and system OpenSlide libraries.
387
+
388
+ ## FOV Recalculation Rules
389
+
390
+ When rebuilding transcript FOV assignments, `xenium-splitter` reads
391
+ `instrument_sw_version` from `experiment.xenium` and uses these FOV dimensions
392
+ (in pixels):
393
+
394
+ - `< 1.2`: `4240` rows x `2960` cols
395
+ - `>= 1.2`: `3520` rows x `2960` cols
396
+
397
+ The FOV overlap is fixed at `128` pixels.
398
+
399
+ - X stride: `2960 - 128 = 2832`
400
+ - Y stride (`< 1.2`): `4240 - 128 = 4112`
401
+ - Y stride (`>= 1.2`): `3520 - 128 = 3392`
402
+
403
+ Potential FOV counts in each direction are computed from rebased transcript
404
+ pixel coordinates (`x_px`, `y_px`) as:
405
+
406
+ - `potential_fov_x = floor(max(x_px) / x_stride) + 1`
407
+ - `potential_fov_y = floor(max(y_px) / y_stride) + 1`
408
+
409
+ where `x_px = floor(x_um / pixel_size_um)` and `y_px = floor(y_um / pixel_size_um)`.
410
+
411
+ For example, with `instrument_sw_version: 1.1.2.4`, the splitter uses the
412
+ `< 1.2` rule: `4240 x 2960` FOVs, overlap `128`, strides `4112` (Y) and `2832` (X).
413
+
414
+ ## Development
415
+
416
+ ```bash
417
+ pytest
418
+ ruff check .
419
+ ```