Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

fturader

RF-DETR + SAM2 pipeline for FTU (Functional Tissue Unit) instance segmentation in histopathology images — H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) via a Beer-Lambert pseudo-H&E front end.

One call turns a tissue image into an (H, W) integer FTU label map — segment_ftu(image, channel_names, tissue_type). Install → drop in the weights → run.


Requirements

RequirementVersion
Python3.10
NVIDIA GPU + CUDA≥ 11.8
torch≥ 2.5.1
rfdetr≥ 1.6.5 (validated on 1.8.0)
SAM-2install from GitHub (pulled in automatically as sam-2@git+…)
supervision≥ 0.26
tifffile≥ 2024.1

The DINOv2 patch-size / positional-encoding warnings printed when RF-DETR loads a checkpoint are benign and version-independent — the FTU checkpoints were trained at patch_size=16 / resolution=1024 (not stock DINOv2's 14/518), so RF-DETR (any version) reports it is not loading stock backbone weights. The full RF-DETR weights load fine.


Installation

From source (editable):

pip install -e .[dev] # [dev] adds pytest, jupyter; omit for runtime only

This pulls torch, rfdetr, supervision, tifffile, and installs SAM-2 from GitHub (sam-2@git+https://github.com/facebookresearch/sam2.git), so the install host needs network access to GitHub and a working C/CUDA toolchain for SAM-2's extensions. The optional [zarr] extra (pip install -e .[zarr]) adds lazy windowed reads for very large arrays.

Pre-trained weights

Running inference requires pre-trained model weights under weights_root (DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1/, also the CLI's default --weights-root). The directory must contain per-organ RFDETRLarge_resolution_1024_<organ>.pth (kidney / largeintestine / lung / prostate / spleen) and a shared sam2.1_hiera_b+_epoch_300.pth. Two ways to get them, pick one:

1. Place local weights (no token). If you already hold the .pth files, drop them into ~/.deepcell/models/ftusam_v0.1/. segment_ftu and the CLI load from there by default — no network, no token.

2. Download from Deepcell (needs a token). Create an access token at users.deepcell.org, then set it as an environment variable in your shell (not in a notebook or source) and download:

export DEEPCELL_ACCESS_TOKEN=<your-token>
python -c "import fturader; fturader.download_model_weights()"# → ~/.deepcell/models/ftusam_v0.1/

download_model_weights() fetches and unpacks the archive into ~/.deepcell/models/ftusam_v0.1/. Authentication goes through the DEEPCELL_ACCESS_TOKEN environment variable (_auth.py); a missing token raises a clear ValueError pointing back to users.deepcell.org.

Security: never commit a token or write it into a notebook cell / source file. Use a shell environment variable (or a secrets manager). The download path has not been smoke- tested here because no token was available; the local-weights path is the one exercised.

Smoke test (no GPU / no weights)

Verify the install can import and that the CLI parses, without a GPU or weights:

python -c "import fturader; from fturader.multiplex import SingleImageDataset, infer_multiplex; print('import OK')"
fturader --help
pytest -q -m unit # fast, no GPU, no external checkpoints

A weight-free end-to-end dry run (channel classification only, no model) on any multichannel OME-TIFF:

fturader --input stitched.ome.tif --print-classification

Supported Organs (defaults)

Organmin_mask_area (px²)score_threshold
prostate400.33
largeintestine2000.36
lung1 0000.25
kidney5 0000.25
spleen10 0000.38

Lung default raised in R3 (score_threshold 0.13 → 0.25; min_mask_area1 000). The old 0.13 produced 400+ detections per crop with slow NMM merge; 0.25 gives cleaner, faster output (a 6000²-px crop @ 0.25 ≈ 219 FTUs / 41 s) with no hand-set threshold.

Tuning for fewer, cleaner detections: raise score_threshold and/or min_mask_area. CLI: --score-threshold / --min-mask-area (0 = organ default). Python: segment_ftu(..., score_threshold=…, min_mask_area=…) or any infer_wsi kwarg.


Quick start — segment_ftu

The shortest path to a result. Hand us a multichannel image as (C, H, W), the channel names, and the tissue type; get back an (H, W) integer FTU label map. This is the most general entry pointrecipe="auto" chooses the pseudo-H&E channels per panel, and the model always runs the large-image tiling engine, so it works on the big mosaics (thousands of px on a side) that this pipeline targets.

importnumpyasnpfromfturaderimportsegment_ftu# A multiplex fluorescence mosaic: C channels, then height, width.image= ... # np.ndarray, shape (C, H, W); C is unrestrictedchannel_names= ["Hoechst1", "Cytokeratin", "Vimentin", "CollIV", ...] # len == Clabels=segment_ftu(
image,
channel_names,
tissue_type="largeintestine", # one of SUPPORTED_ORGANS (selects the RF-DETR weights)recipe="auto", # auto-pick channels (recipes: docs/python_api.md)pixel_size_um=0.377, # µm/px — pass it; detections are scaled by it
)
# labels: (H, W) int, 0 = background, 1..N = FTU instances

segment_ftu packages from_image → pseudo-H&E synthesis → tissue_type → weights → infer_wsi → rasterize labels. It needs pre-trained weights present under weights_root (see Pre-trained weights).

Useful options (full reference in src/fturader/api.py docstring):

ArgumentMeaning
mask=optional (H, W) boolean ROI; output labels are intersected with it (outside → 0). None = whole image.
recipe="auto" (default) / "auto:mean" / "auto:max" / "ck" / an eosin marker list e.g. ["Cytokeratin", "Vimentin"] / a PseudoHEConfig. "v1" / "v2" are deprecated aliases.
he=None (default) synthesizes a named fluorescence panel; True passes an already-H&E (H, W, 3) image through unchanged. Channel count never decides H&E — he does.
pixel_size_um=µm/px; None falls back to 0.4 with a synthesis warning.
weights_root=weights directory; default DEFAULT_WEIGHTS_ROOT = ~/.deepcell/models/ftusam_v0.1.
return_report=Truealso return a machine-readable report dict {"load": …, "synthesis": …, "infer": …} (loader facts + structured synthesis config + inference plan with n_detections).
**infer_kwforwarded to infer_wsi (e.g. score_threshold, overlap_ratio); the organ default score_threshold is used otherwise.

Client notes (read before running).

  • Working size: best at roughly 3000–10000 px on the long side. Far smaller crops give the tiler too little context; far larger only costs time.
  • auto can pick poorly on unusual panels. If the pseudo-H&E looks washed out, inspect describe() (via return_report=True) and specify channels manually — pass an eosin marker list, e.g. recipe=["Cytokeratin", "Vimentin"], or recipe="ck" for an epithelial organ that carries Cytokeratin.
  • Resource cost (U5–U9 profiling): VRAM ~1.4 GB; host RAM peak scales with area × channel count, roughly 16–35 GB for a ~10000²-px multi-channel mosaic; wall time scales with the number of detected FTUs.

For finer control over loading (separate tile files, in-memory arrays, block-wise focus) or for the infer_image / infer_wsi model objects directly, see docs/python_api.md.

Transparency: report & profiling

Three ways to see what a run will (or did) do — pick by what you want:

wantcallshape
human-readable summary of what synthesis will dodataset.describe()concise string (one load line + one synthesis line); describe(verbose=True) adds the full channel list, normalization, Beer-Lambert and best-focus z
machine-readable transparency bundlesegment_ftu(..., return_report=True)one dict {"load": …, "synthesis": …, "infer": …}
just the tiling/route plan (no model run beyond geometry)model.infer_plan(image_hw, pixel_size_um)the infer section standalone (no pixels read, no inference run)

dataset.describe() covers the load + synthesis stages only; the infer stage (tiling, route, threshold, detection count) needs the model, so it lives in return_report=True / infer_plan. (The structured dict behind describe() is the private _describe_synthesis, exposed programmatically as report["synthesis"].)

Profiling — where wall time goes (and the NMM bottleneck). Pass profile=True to record per-stage timings:

model.infer_wsi(rgb, pixel_size_um=0.377, profile=True)
prof=model.last_profile# dict of per-stage seconds# or, via the one-call entry:labels, report=segment_ftu(..., profile=True, return_report=True)
prof=report["infer"]["profile"]

The CLI writes the same block as profile into the bundle summary.json. Keys: scale_s, n_tiles, tile_forward_s (= RF-DETR + SAM2 tile inference), assemble_s (cross-tile concat), merge_s (the NMM/NMS dedup — timed directly by wrapping sv.Detections.with_nmm), slicer_overhead_s, rescale_s, total_s, n_detections.

Empirical conclusion (R3 runs):tile_forward_s (inference) stays bounded at ~2–19 s across samples, while merge_s (the NMM merge) scales with detection count and dominates wall time at high counts:

samplen_detectionsmerge_s
HBM57360.02 s
CODEX (large intestine)~26~1.0 s
lung 6000²-px @ 0.25~219~27 s
HBM288 full24799 s (≈80 % of a 124 s run)

8× more detections (26 → 209) cost ~46× the merge time, with inference roughly unchanged. This is why raising score_threshold / min_mask_area makes runs both cleaner and faster.


Notebooks

Notebooks come in two tiers — start minimal, then go comprehensive:

TierPythonCLI
Getting started (minimal — one call, copy-paste)notebooks/getting_started_python.ipynb — a single segment_ftu callnotebooks/getting_started_cli.ipynb — one fturader command, with the 3 key params (--pixel-size / --channel-names / --he) explained up front
Comprehensive (full tour)notebooks/walkthrough_python.ipynbsegment_ftu, H&E + CODEX + OME, recipe comparison, bundle I/Onotebooks/walkthrough_cli.ipynb — the CLI equivalents

Read a getting-started notebook first for the shortest working path, then move to the matching comprehensive guide for the full feature tour. The kidney example was removed from the notebooks (kidney remains a fully supported organ in the API and the defaults table). Deprecated notebooks are kept under notebooks/legacy/.

The getting-started notebooks run on a small bundled crop with no download. The walkthrough notebooks are best read as a reference tour — every cell runs on the full raw HuBMAP slides, so to actually re-execute them you must first download the datasets they list (HuBMAP Data Portal links are in each walkthrough).


CLI

The unified fturader command handles both H&E brightfield and multiplex fluorescence (CODEX / OME-TIFF) inputs through a single --input. It always runs infer_wsi internally (tiling kicks in for large inputs).

Deprecated aliases kept for backward compatibility: fturader-infer (H&E only) and fturader-multiplex (multiplex only) share the same logic as fturader and will be removed in a future release.

Full parameter reference: docs/cli.md.

Quick examples

# H&E brightfield (kidney)
fturader \
--input /path/to/image.ome.tiff \
--he true \
--organ kidney \
--output-dir ./out/ \
--save-overlay
# Multiplex CODEX directory (large intestine; manual eosin channels, or recipe=ck)
fturader \
--input /path/to/processed/ \
--channel-names /path/to/extras/channelnames.txt \
--pixel-size 0.377 \
--recipe Cytokeratin,Vimentin \
--organ largeintestine \
--output-dir ./out/
# Already-stitched OME-TIFF (channel names + pixel size read from OME-XML)
fturader \
--input stitched.ome.tif \
--recipe auto \
--organ largeintestine \
--output-dir ./out/
# Dry run: print channel classification only (no GPU / no weights)
fturader --input stitched.ome.tif --print-classification

Output bundle

FileDescription
labels.tif(H, W) uint16/uint32 instance map (zlib-compressed); 0=background, 1..N; high-confidence on top
detections.npzcompact-RLE per-instance masks + xyxy / confidence / rf_score / sam_score
summary.jsonorgan, pixel_size_um, n_detections, overlap_strategy, checkpoint paths, pseudo_he_report (effective pseudo-H&E config), …
overlay_full.jpgexp4-style turbo filled-mask overlay (--save-overlay)
overlay_2000.jpgsame, short-side-2000px JPEG (--save-overlay)

Reload in Python:

fromfturader.ioimportload_detectionsdets, meta=load_detections("./out/") # sv.Detections + metadata dict

Results

Instance segmentation (COCO AP / F1@)

F1 and COCO AP per organ

Segmentation quality (pixel-level Dice & IoU)

Dice and IoU violin plots per organ

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages