Skip to content

Add body envelope multi table - #42

Merged
Can-Zhao merged 12 commits into
NVIDIA-Medtech:mainfrom
Can-Zhao:add-body-envelope-multi-table
Jul 20, 2026
Merged

Add body envelope multi table#42
Can-Zhao merged 12 commits into
NVIDIA-Medtech:mainfrom
Can-Zhao:add-body-envelope-multi-table

Conversation

@Can-Zhao

Copy link
Copy Markdown
Contributor

No description provided.

Can-Zhao and others added 12 commits July 18, 2026 22:30
The find-air-invert steps can leak the air-density CT table into the body
(air trapped between patient and table is a separate component from the
exterior air, so it reads as "not air" -> body). Step 8 detects it as the
largest connected component of body voxels that are actually air
(CT < hu_threshold) and drops it when that component is >= table_frac_thresh
(default 0.05) of the body. Since the seg labels the lungs, no legitimate
air region is table-sized (a table is ~16-28% of body vs <0.3% clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hu_threshold)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eps 1&4) + log when table safety net fires

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tep-8

A CT table can split into multiple air-in-body connected components (side rails,
pads, or a table broken by the patient silhouette). The previous step-8 removed
only the single largest component, leaving the others labeled as body. Replace
the largest-CC selection with scipy.ndimage.label over the full air-in-body mask
and drop every component >= table_frac_thresh of the body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 8 removes large air-density regions inside the body as CT table. The lungs
are the only legitimate large air pocket, so they must already be labeled in the
input seg (nv-segment-ct / VISTA everything_labels) — otherwise unsegmented lung
air reads as a table-sized component and is wrongly removed. Add an .. important::
note to the docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 8 assumes the lungs are labeled (they're the only legitimate large air-in-body
region). Add seg_has_lung (default True): True runs step 8; False skips it so a
segmentation without lungs does not have its lung air mistaken for the table and
removed. Also drop the pengfeig/3d_ldm_monai provenance line from the docstring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_has_lung arg doc)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…age import to top

Wrap the entire step-8 block (air-mask computation + labeling) in 'if seg_has_lung'
so nothing is computed when lungs are absent, instead of computing air_hu/air_body/
n_body and then discarding them. Move 'from scipy import ndimage' to the module-level
imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….py (line fits in 150)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Can-Zhao Can-Zhao self-assigned this Jul 20, 2026
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the add_body_envelope function to handle CT tables that split into multiple disconnected pieces (side rails, pads, broken by patient silhouette) — replacing the previous single-largest-component removal with a loop that drops every air-in-body component above the size threshold. A new seg_has_lung parameter guards step 8 so that cases without lung labels don't misidentify lung air as table.

  • Multi-component table removal: replaces get_largest_connected_component_mask + single threshold check with scipy.ndimage.label + np.bincount + np.nonzero to identify and remove all oversized air-in-body components.
  • seg_has_lung flag (default True): skips step 8 entirely when the segmentation has no lung labels, preventing lung air from being misclassified as table and removed.
  • String refactor: one log message in infer_image_from_mask.py is joined to a single line; no logic change.

Confidence Score: 4/5

Safe to merge for the common case; the only concern is a silent change in connected-component connectivity that could cause a diagonally-bridged table blob to be under-detected.

The multi-component table removal logic is correct and well-guarded. The connectivity change from 26 to 6 is the only notable concern, and in practice large air blobs are almost always face-connected.

scripts/utils.py — specifically the ndimage.label call in step 8 and whether the default 6-connectivity is intentional.

Important Files Changed

Filename Overview
scripts/utils.py Extends add_body_envelope: removes all table-sized air components (not just largest), adds seg_has_lung guard, switches from MONAI's get_largest_connected_component_mask (26-connected) to scipy.ndimage.label (6-connected by default) — a subtle connectivity mismatch with the previous implementation.
scripts/infer_image_from_mask.py Trivial string formatting change: joins two-line log message into a single line; no logic change.

Reviews (1): Last reviewed commit: "ruff format: collapse a pre-split log st..." | Re-trigger Greptile

Comment thread scripts/utils.py
out[table] = 0 # remove the detected table from the body
print(f"[add_body_envelope] table detected ({100.0 * n_table / n_body:.1f}% of body) -> removed", flush=True)
if air_body.any() and n_body:
lbl, ncc = ndimage.label(air_body) # all air-in-body components

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Connectivity mismatch with original code

scipy.ndimage.label uses 6-connectivity (face-adjacent only) by default in 3D, because structure=None resolves to generate_binary_structure(rank, 1). The original get_largest_connected_component_mask(connectivity=None) called skimage.measure.label(connectivity=None), which in 3D defaults to full 26-connectivity (all face + edge + corner neighbors). If a table blob is connected only diagonally — which can happen at typical CT voxel resolutions where the air pocket narrows to a single-voxel diagonal bridge — 6-connectivity will split it into separate components. Each piece could then fall below table_frac_thresh and escape removal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that means you need to tune table_frac_thresh?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite — tuning the threshold doesn't fully solve it. The problem is that a single physical table blob can be split into an unpredictable number of pieces (2, 3, or more depending on scan resolution and patient geometry). You don't know the split factor in advance, so there's no safe threshold to tune to.

Worse, lowering table_frac_thresh to catch split pieces risks false positives: bowel gas, stomach air, or other small legitimate air-in-body pockets could exceed a lower threshold and get incorrectly removed.

The direct fix is to match the original 26-connectivity behavior:

Suggested change
lbl, ncc = ndimage.label(air_body) # all air-in-body components
lbl, ncc = ndimage.label(air_body, structure=ndimage.generate_binary_structure(3, 3)) # all air-in-body components

This makes ndimage.label use face+edge+corner neighbors (same as skimage.measure.label(connectivity=None) in the original code), so a diagonally-bridged table stays as one component and the existing table_frac_thresh logic works as intended.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@Can-Zhao
Can-Zhao merged commit acc49b3 into NVIDIA-Medtech:main Jul 20, 2026
2 checks passed
@Can-Zhao
Can-Zhao deleted the add-body-envelope-multi-table branch July 20, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant