Metadata-Version: 2.4
Name: thymus-quant
Version: 0.1.0a4
Summary: Thymic quantification toolkit
Author: Yuki Okamura
License-Expression: Apache-2.0
Project-URL: Repository, https://github.com/yuki-okamura-med/thymus-quant
Project-URL: Issues, https://github.com/yuki-okamura-med/thymus-quant/issues
Keywords: thymus,CT,segmentation,quantification,medical-imaging
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: huggingface_hub>=0.23
Requires-Dist: monai>=1.3
Requires-Dist: numpy>=1.24
Requires-Dist: nibabel>=5.0
Requires-Dist: pandas>=2.0
Requires-Dist: safetensors>=0.4
Requires-Dist: SimpleITK>=2.3
Requires-Dist: scipy>=1.10
Requires-Dist: torch>=2.0
Requires-Dist: torchvision>=0.15
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# thymus-quant

This package lets you quantify thymic tissue contained within the thymic region
on human CT images. It implements the framework described in Okamura YT et
al., Ann Biomed Eng, 2025. http://dx.doi.org/10.1007/s10439-025-03805-z

From version 0.1.0a4, thymus-quant checks the voxel order (orientation) of the
input image and, when needed, reorders the voxels to the order used for training
(LAS) before segmentation. Earlier versions used the voxel array as stored in the
file. If your NIfTI files were not in LAS order (for example, files written by
ITK or SimpleITK, which are usually LPS, or files reoriented to RAS), the
segmentation with earlier versions may have been poor, and we recommend running
those images again with 0.1.0a4 or later. You can check the voxel order of a file
with `nibabel.aff2axcodes(nibabel.load(path).affine)`; see
[Voxel order (orientation)](#voxel-order-orientation) for details.

Main readouts:

- $A_{TRQ}$: representative HU value of the thymic region of quantification.
- $V_{TRQ}$: volume of the thymic region of quantification.
- **Estimated Thymic Volume (ETV)**: estimated thymic tissue volume in mL.
- Thymic tissue fraction: fraction of thymic tissue in the thymic region,
  reported as a 0-1 value.

Main APIs:

- `segment_trq`
- `quantify`
- `analyze`
- `analyze_many`

## Installation

Install from PyPI:
```bash
pip install thymus-quant
```

This installs the dependencies needed for both quantification and TRQseg-v1
segmentation.

Model weights are loaded through the Hugging Face cache unless `local_files_only`
or an explicit local mirror is configured. A local TRQseg-v1 mirror may be set
with `THYQ_TRQSEG_V1_LOCAL_REPO=/path/to/TRQseg-v1`.

## Minimal Example

```python
import thymus_quant as tq

result = tq.analyze(
    "case.nii.gz",
    method="okamura",
    segmentor="trqseg_v1",
    study_id="case-001",
)

print(result.thymic_tissue_fraction)
print(result.etv_ml)
print(result.qc.status)
```

`thymic_tissue_fraction` is the Thymic Tissue Fraction. It is reported as a 0-1
fraction, not as a percentage.
`etv_ml` and `trq_volume_ml` are in mL.

## Reproducible Model Loading

```python
segmentor = tq.load_segmentor(
    "trqseg_v1",
    revision="<immutable-tested-revision>",
    device="auto",
)
```

`device` controls where TRQseg-v1 model inference runs. Supported values are
`"auto"`, `"cpu"`, and `"cuda"`. `"auto"` selects CUDA when PyTorch detects an
available CUDA device, and otherwise falls back to CPU.

When weights are loaded from Hugging Face, branch/tag/default revisions are
resolved to the underlying commit SHA before download and recorded in result
metadata. When all selected weights are loaded from a local git mirror, the local
mirror's HEAD commit is recorded instead. The result metadata distinguishes
requested and resolved revision and records the weight source (`local`,
`downloaded`, `mixed`, or `none`).

## Two-Stage Workflow

```python
seg = tq.segment_trq(
    "case.nii.gz",
    segmentor=segmentor,
)

result = tq.quantify(
    seg,
    method="okamura",
)
```

`segmentor` is always explicit. Omitting it never selects the heuristic backend.

## Existing Mask

Single external mask:

```python
seg = tq.SegmentationResult.from_mask(
    trq_mask=mask,
    ct_hu=ct_hu,
    spacing_mm=(0.7, 0.7, 1.0),
    study_id="case-001",
)

result = tq.quantify(seg, method="okamura")
```

Okamura member ensemble:

```python
seg = tq.SegmentationResult.from_member_masks(
    member_masks=[mask0, mask1, mask2, mask3, mask4],
    ct_hu=ct_hu,
    spacing_mm=(0.7, 0.7, 1.0),
    study_id="case-001",
    member_ids=["fold-0", "fold-1", "fold-2", "fold-3", "fold-4"],
)

result = tq.quantify(seg, method="okamura")
```

## Inputs

Accepted image inputs for segmentation are NIfTI path-like values and nibabel
spatial images. Existing masks can be supplied with NumPy-compatible arrays via
`SegmentationResult.from_mask` or `from_member_masks`.

Data contract:

- CT values must be HU.
- CT arrays must be 3D.
- Masks must be 3D bool or binary 0/1 arrays.
- CT and mask shapes must match exactly.
- `spacing_mm` is 3 positive finite values in millimeters.
- `spacing_mm` is normalized to Python floats and used for voxel-volume
  calculation. It is not changed; its difference from the affine column norms
  is recorded in the geometry report (see below).
- External CT/mask inputs are not resampled, cropped, padded, flipped, or
  permuted. Shape mismatches raise `InputValidationError` instead of being
  repaired automatically.
- Masked CT voxels must contain at least one finite HU value.
- DICOM directories are not directly supported by the public API.
- Current TRQseg-v1 preprocessing records input/output shapes and returns masks
  on the input grid. Inputs outside the reference preprocessing domain should be
  treated cautiously until a pinned regression fixture exists.

### Voxel order (orientation)

TRQseg-v1 segments each axial slice as a 2D image and was trained on NIfTI files
converted from DICOM with dcm2niix, which stores axial CT in LAS voxel order
(first array axis toward the patient's left, second toward anterior, third toward
superior; `nibabel.aff2axcodes(img.affine) == ("L", "A", "S")`). The network does
not read the affine, so an array stored in another order (for example LPS, the
usual result of converting DICOM with ITK/SimpleITK, or RAS after
`nibabel.as_closest_canonical`) is seen flipped or permuted.

- For TRQseg-v1 segmentation, when the input orientation is known and is not LAS,
  the voxel array is permuted/flipped to LAS for the network only, and the
  predicted masks are permuted/flipped back. No interpolation is done. Returned
  masks are on the input grid, in the input voxel order.
- Orientation is taken from the affine of NIfTI/nibabel inputs, or from
  `ImageContext.orientation` / `make_image_context(..., orientation=...)`.
  Arrays without affine or orientation are used as given (assumed LAS).
- LAS inputs take the same path as before, with identical results.
- Oblique or sheared affines are not resampled. Their geometry is recorded.
- The handling is recorded in `SegmentationResult.preprocessing` and in the
  result metadata: `model_orientation`, `orientation_status`
  (`model_orientation`, `reoriented`, `assumed_model_orientation`, `unresolved`),
  and `reoriented_for_model`. The same information is in the `input_orientation`,
  `orientation_status` and `reoriented_for_model` columns of `to_record()` /
  `to_frame()`. Reorientation is logged at INFO level by the
  `thymus_quant.segmentors` logger.
- `geometry` (in `ImageContext`, `SegmentationResult.to_dict()["image"]` and the
  result metadata) records `voxel_sizes_affine_mm`, `voxel_size_mismatch_max_mm`,
  `obliquity_max_deg`, `shear_max`, and, for NIfTI images, `qform_code`,
  `sform_code` and `qform_sform_max_abs_diff`. These values are reported only;
  no thresholds are applied.
- The heuristic segmentor (`heuristic_trq`, debug only) is not affected.

### In-plane size

The TRQseg-v1 training images were 512 x 512, and the network sees each slice
downsampled by 2 (`ct[::2, ::2, :]`). Images of other in-plane sizes are
segmented as given; they are not padded, cropped or resampled.

- The in-plane size is checked after reorientation to LAS. When it is not
  512 x 512, a WARNING is logged by the `thymus_quant.segmentors` logger and a
  note is added to `result.warnings`. The note does not change `flags`,
  `qc.status` or `qc.paper_criteria_met`.
- `SegmentationResult.preprocessing`, the result metadata and the `to_record()` /
  `to_frame()` columns record `inplane_shape`, `inplane_is_512` and
  `network_pixel_mm` (the in-plane pixel size seen by the network, that is,
  2 x the voxel spacing).
- What matters most for the network is the pixel size (how large the anatomy
  appears), not the matrix size itself. In our checks on two scans, padding a
  512 x 512 image to 640 x 640 or cropping it to 448 x 448 changed the fold-0
  mask only slightly (Dice 0.988 to 0.995 against the 512 x 512 result).

## Segmentor Options

Built-in aliases:

- `trqseg_v1`: DeepLabV3-ResNet50 5-member TRQseg-v1 ensemble.
- `heuristic_trq`: debug/experimental heuristic only, not a research model.

Supported options include revision pinning, `members`/fold selection, `device`,
`local_files_only`, and `cache_dir`. Unknown segmentor strings are rejected and
are not interpreted as arbitrary Hugging Face repository IDs. Empty, duplicate,
negative, or out-of-range members are rejected. Model load failure does not fall
back to `heuristic_trq`.

## Result Glossary

- TRQ: thymic region of quantification.
- `A_TRQ`: representative TRQ attenuation used by published formulas.
- `V_TRQ`: TRQ volume.
- Thymic Tissue Fraction: fraction of thymic tissue in the TRQ, usually 0-1.
- ETV: estimated thymic volume, unit mL.
- `thymic_tissue_fraction`: summary Thymic Tissue Fraction for the study.
- `etv_ml`: summary ETV in mL.
- `trq_volume_ml`: summary TRQ volume in mL.

Okamura member fields are per-member values. Summary fields are ensemble
aggregates. QC-invalid members can still retain numeric values; check
`result.status`, `result.flags`, and `result.qc.flags` before using them.

## QC and Errors

Statuses are intentionally simple:

- `ok`: no relevant QC flags.
- `check`: numeric values may exist but require review.
- `failed`: no computable result.
- `not_available`: not applicable.

Important flags include `invalid_member`, `all_members_invalid`,
`used_invalid_members`, `kde_failed`, and `ensemble_qc_unavailable`.

For the Okamura method, a non-unimodal member distribution is marked invalid
with `multimodal_or_invalid`. By default, any invalid member adds
`invalid_member` and makes the study status `check`. If at least one member
passes QC, the summary uses only QC-valid members. If all members fail QC but
still have computable numeric values, the summary uses those invalid members,
sets `all_members_invalid` and `used_invalid_members`, and remains `check`.

KDE computation failure is not converted to a fake median mode. Missing spacing
does not produce a successful result with NaN volume/ETV output. Single-member
analysis can return quantification values, but 5-member paper ensemble criteria
are not passed and `ensemble_qc_unavailable` is set.

`failed` means at least one member/mask was provided, but no computable Okamura
value was produced. `not_available` means the method had no applicable
member/mask to evaluate; this should be uncommon when using the public
constructors.

Batch processing preserves input order and records errors with input index:

```python
batch = tq.analyze_many(paths, method="okamura", segmentor=segmentor, on_error="record")
errors = batch.errors_to_frame()
```

`on_error` values are `raise`, `record`, and `skip`.

## Coming soon: Expanded thymic composition analysis workflows.

Recent work by Chaunzwa et al. introduced an enhanced thymic composition analysis framework that builds upon and extends the methodology currently implemented in this package (Chaunzwa et al., medRxiv, 2025: https://doi.org/10.1101/2025.10.27.25338565, https://doi.org/10.1101/2025.10.20.25338395). Support for this framework is planned for a future release.
