lavlab-shell 0.3.2__tar.gz → 0.4.0__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 (40) hide show
  1. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/PKG-INFO +1 -1
  2. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/__about__.py +1 -1
  3. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/cli.py +63 -2
  4. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/infer_omero_wsi.py +49 -8
  5. lavlab_shell-0.4.0/src/shell/infer_wsi.py +796 -0
  6. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/inference.py +27 -15
  7. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/model.py +14 -3
  8. lavlab_shell-0.4.0/src/shell/post_process.py +1349 -0
  9. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/weights/model_v1.pth +0 -0
  10. lavlab_shell-0.3.2/src/shell/infer_wsi.py +0 -373
  11. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.devcontainer/Dockerfile +0 -0
  12. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.editorconfig +0 -0
  13. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.github/dependabot.yml +0 -0
  14. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.github/workflows/build.yml +0 -0
  15. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.github/workflows/lint.yml +0 -0
  16. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.github/workflows/publish.yml +0 -0
  17. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.github/workflows/pytest.yml +0 -0
  18. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.gitignore +0 -0
  19. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/.pre-commit-config.yaml +0 -0
  20. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/CONTRIBUTING.md +0 -0
  21. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/Dockerfile +0 -0
  22. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/LICENSE.txt +0 -0
  23. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/Makefile +0 -0
  24. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/README.md +0 -0
  25. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/docs/api.md +0 -0
  26. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/docs/index.md +0 -0
  27. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/mkdocs.yml +0 -0
  28. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/pyproject.toml +0 -0
  29. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/requirements/requirements-docs.txt +0 -0
  30. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/requirements/requirements-lint.txt +0 -0
  31. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/requirements/requirements-test.txt +0 -0
  32. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/requirements/requirements-types.txt +0 -0
  33. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/requirements.txt +0 -0
  34. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/__init__.py +0 -0
  35. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/benchmark.py +0 -0
  36. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/py.typed +0 -0
  37. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/src/shell/transforms.py +0 -0
  38. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/tests/__init__.py +0 -0
  39. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/tests/conftest.py +0 -0
  40. {lavlab_shell-0.3.2 → lavlab_shell-0.4.0}/tests/test_shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lavlab-shell
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: SHELL Highlights Epithelium and Lumen Locations — whole-slide H&E segmentation
5
5
  Project-URL: Documentation, https://github.com/laviolette-lab/shell#readme
6
6
  Project-URL: Issues, https://github.com/laviolette-lab/shell/issues
@@ -3,4 +3,4 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
  """Version information for shell."""
5
5
 
6
- __version__ = "0.3.2"
6
+ __version__ = "0.4.0"
@@ -105,7 +105,7 @@ def build_parser() -> argparse.ArgumentParser:
105
105
  infer_p.add_argument(
106
106
  "--target-mpp",
107
107
  type=float,
108
- default=1.0,
108
+ default=2.0,
109
109
  help="Desired output resolution (um/pixel).",
110
110
  )
111
111
  infer_p.add_argument(
@@ -125,6 +125,51 @@ def build_parser() -> argparse.ArgumentParser:
125
125
  default=None,
126
126
  help="Optional: also save the intermediate EHO image.",
127
127
  )
128
+ infer_p.add_argument(
129
+ "--save-raw",
130
+ type=str,
131
+ default=None,
132
+ help=(
133
+ "Optional: save raw model predictions as a 3-band uint8 image "
134
+ "(band 0 = inner / lumen, band 1 = outer / epithelium, "
135
+ "band 2 = background), scaled to 0/255. "
136
+ "Saved at the original input resolution. "
137
+ "Useful for debugging or re-running post-processing offline."
138
+ ),
139
+ )
140
+ infer_p.add_argument(
141
+ "--profile",
142
+ type=str,
143
+ default="best_effort",
144
+ choices=["best_effort", "precise", "sensitive"],
145
+ help=(
146
+ "Post-processing filter profile. "
147
+ "'best_effort' (default) balances sensitivity and precision; "
148
+ "'precise' is more conservative; 'sensitive' keeps more predictions."
149
+ ),
150
+ )
151
+ infer_p.add_argument(
152
+ "--mode",
153
+ type=str,
154
+ default="wsi",
155
+ choices=["wsi", "biopsy", "tile"],
156
+ help=(
157
+ "Post-processing mode. "
158
+ "'wsi' (default): full pipeline with tissue restriction and urethra detection. "
159
+ "'biopsy': tissue restriction but no urethra detection. "
160
+ "'tile': no tissue mask/urethra; reflect-pads predictions before morphological ops. "
161
+ "Use 'tile' for individual image tiles that lack surrounding context."
162
+ ),
163
+ )
164
+ infer_p.add_argument(
165
+ "--tile-pad",
166
+ type=int,
167
+ default=None,
168
+ help=(
169
+ "Reflect-padding in pixels applied on each side in tile mode. "
170
+ "Defaults to 50%% of the shorter output dimension when not set."
171
+ ),
172
+ )
128
173
  infer_p.add_argument(
129
174
  "--device",
130
175
  type=str,
@@ -156,7 +201,7 @@ def build_parser() -> argparse.ArgumentParser:
156
201
  omero_p.add_argument(
157
202
  "--target-mpp",
158
203
  type=float,
159
- default=1.0,
204
+ default=2.0,
160
205
  help="Desired output resolution (um/pixel)",
161
206
  )
162
207
  omero_p.add_argument(
@@ -252,6 +297,17 @@ def build_parser() -> argparse.ArgumentParser:
252
297
  "sessions. Default 4."
253
298
  ),
254
299
  )
300
+ omero_p.add_argument(
301
+ "--profile",
302
+ type=str,
303
+ default="best_effort",
304
+ choices=["best_effort", "precise", "sensitive"],
305
+ help=(
306
+ "Post-processing filter profile. "
307
+ "'best_effort' (default) balances sensitivity and precision; "
308
+ "'precise' is more conservative; 'sensitive' keeps more predictions."
309
+ ),
310
+ )
255
311
 
256
312
  return parser
257
313
 
@@ -303,6 +359,10 @@ def main(argv: list[str] | None = None) -> int:
303
359
  target_mpp=args.target_mpp,
304
360
  mpp=args.mpp,
305
361
  save_eho=args.save_eho,
362
+ save_raw=args.save_raw,
363
+ profile=args.profile,
364
+ mode=args.mode,
365
+ tile_pad=args.tile_pad,
306
366
  device=args.device,
307
367
  )
308
368
 
@@ -351,6 +411,7 @@ def main(argv: list[str] | None = None) -> int:
351
411
  min_tissue_frac=args.min_tissue_frac,
352
412
  prefetch_depth=args.prefetch_depth,
353
413
  num_fetch_workers=args.fetch_workers,
414
+ profile=args.profile,
354
415
  )
355
416
  log.debug("DIAG: infer_omero_wsi returned normally")
356
417
  except BaseException as e:
@@ -1292,6 +1292,7 @@ def infer_omero_wsi(
1292
1292
  min_tissue_frac: float = 0.01,
1293
1293
  prefetch_depth: int = 8,
1294
1294
  num_fetch_workers: int = 4,
1295
+ profile: str = "best_effort",
1295
1296
  ) -> np.ndarray:
1296
1297
  """Tile-based OMERO inference pipeline.
1297
1298
 
@@ -1633,6 +1634,15 @@ def infer_omero_wsi(
1633
1634
  )
1634
1635
  tiles_tissue = len(tile_schedule)
1635
1636
 
1637
+ # Upscale tissue mask to full resolution for post-processing.
1638
+ th_h, th_w = tissue_mask.shape
1639
+ y_idx = np.clip(
1640
+ (np.arange(out_h) * th_h / out_h).astype(np.int64), 0, th_h - 1,
1641
+ )
1642
+ x_idx = np.clip(
1643
+ (np.arange(out_w) * th_w / out_w).astype(np.int64), 0, th_w - 1,
1644
+ )
1645
+ tissue_mask_full = tissue_mask[y_idx[:, None], x_idx[None, :]]
1636
1646
  del tissue_mask
1637
1647
  gc.collect()
1638
1648
 
@@ -1691,6 +1701,8 @@ def infer_omero_wsi(
1691
1701
  tile_overlap=tile_overlap,
1692
1702
  roi_size=inference_tile_size,
1693
1703
  sw_overlap=sw_overlap,
1704
+ tissue_mask_full=tissue_mask_full,
1705
+ profile=profile,
1694
1706
  num_fetch_workers=num_fetch_workers,
1695
1707
  )
1696
1708
 
@@ -1739,6 +1751,8 @@ def _run_pipeline(
1739
1751
  tile_overlap: int,
1740
1752
  roi_size: int,
1741
1753
  sw_overlap: float,
1754
+ tissue_mask_full: np.ndarray | None = None,
1755
+ profile: str = "best_effort",
1742
1756
  num_fetch_workers: int = 4,
1743
1757
  ) -> np.ndarray:
1744
1758
  """Producer/consumer pipeline: parallel fetch+EHO → inference in main.
@@ -1820,7 +1834,10 @@ def _run_pipeline(
1820
1834
  except Exception:
1821
1835
  logger.exception("Failed to start OMERO fetch thread; continuing.")
1822
1836
 
1823
- pred = np.zeros((out_h, out_w), dtype=np.uint8)
1837
+ # Allocate canvases for inner/outer predictions and hematoxylin.
1838
+ inner_pred = np.zeros((out_h, out_w), dtype=bool)
1839
+ outer_pred = np.zeros((out_h, out_w), dtype=bool)
1840
+ hematoxylin_full = np.zeros((out_h, out_w), dtype=np.uint8)
1824
1841
 
1825
1842
  eho_canvas: np.ndarray | None = None
1826
1843
  if save_eho:
@@ -1840,25 +1857,34 @@ def _run_pipeline(
1840
1857
  if eho_canvas is not None:
1841
1858
  eho_canvas[oy0 : oy0 + oh, ox0 : ox0 + ow] = tile_eho
1842
1859
 
1843
- tile_pred = run_inference(
1860
+ # Store hematoxylin channel (ch1) for nuclei segmentation.
1861
+ hematoxylin_full[oy0 : oy0 + oh, ox0 : ox0 + ow] = tile_eho[:, :, 1]
1862
+
1863
+ inner_tile, outer_tile = run_inference(
1844
1864
  tile_eho,
1845
1865
  model,
1846
1866
  device,
1847
1867
  roi_size=(roi_size, roi_size),
1848
1868
  overlap=sw_overlap,
1869
+ return_raw=True,
1849
1870
  )
1850
1871
  del tile_eho
1851
1872
 
1852
1873
  # Write only the centre-cropped keep region to avoid seams.
1853
- ph, pw = tile_pred.shape[:2]
1874
+ ph, pw = inner_tile.shape[:2]
1854
1875
  ky1 = min(keep_y0 + keep_h, ph)
1855
1876
  kx1 = min(keep_x0 + keep_w, pw)
1856
- kept = tile_pred[keep_y0:ky1, keep_x0:kx1]
1857
1877
  out_y0 = oy0 + keep_y0
1858
1878
  out_x0 = ox0 + keep_x0
1859
- ch, cw = kept.shape[:2]
1860
- pred[out_y0 : out_y0 + ch, out_x0 : out_x0 + cw] = kept
1861
- del tile_pred, kept
1879
+ ch = ky1 - keep_y0
1880
+ cw = kx1 - keep_x0
1881
+ inner_pred[out_y0 : out_y0 + ch, out_x0 : out_x0 + cw] = (
1882
+ inner_tile[keep_y0:ky1, keep_x0:kx1]
1883
+ )
1884
+ outer_pred[out_y0 : out_y0 + ch, out_x0 : out_x0 + cw] = (
1885
+ outer_tile[keep_y0:ky1, keep_x0:kx1]
1886
+ )
1887
+ del inner_tile, outer_tile
1862
1888
 
1863
1889
  processed += 1
1864
1890
  if processed % 25 == 0:
@@ -1876,7 +1902,22 @@ def _run_pipeline(
1876
1902
  if torch.cuda.is_available():
1877
1903
  torch.cuda.empty_cache()
1878
1904
 
1879
- return _save_results(pred, eho_canvas, save_eho, output_path)
1905
+ # ── Post-processing ──────────────────────────────────────────
1906
+ from shell.post_process import post_process
1907
+
1908
+ logger.info("Running post-processing (profile=%s) …", profile)
1909
+ label_map = post_process(
1910
+ inner_pred,
1911
+ outer_pred,
1912
+ tissue_mask_full if tissue_mask_full is not None else np.ones((out_h, out_w), dtype=bool),
1913
+ hematoxylin_full,
1914
+ profile_name=profile,
1915
+ verbose=True,
1916
+ )
1917
+ del inner_pred, outer_pred, hematoxylin_full, tissue_mask_full
1918
+ gc.collect()
1919
+
1920
+ return _save_results(label_map, eho_canvas, save_eho, output_path)
1880
1921
 
1881
1922
  finally:
1882
1923
  fetch_thread.join(timeout=30)