Metadata-Version: 2.4
Name: ctbodycomp
Version: 0.1.4
Summary: Selectable 2D and 3D CT body composition features with separate normalization
License-Expression: PolyForm-Noncommercial-1.0.0
Keywords: CT,body-composition,medical-imaging,feature-extraction
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Image Processing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.22
Requires-Dist: nibabel>=5
Requires-Dist: pandas>=2
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# ctbodycomp

CT体成分2D/3D特征提取与独立校正。支持按类别或单个名称选择基础特征，以及独立的身高、体重、BSA和骨参照校正。

## 安装

在本项目目录运行`python -m pip install .`。开发时使用`python -m pip install -e .`。
支持Python 3.10及以上，依赖numpy、nibabel、pandas；测试需要pytest。

## 四类基础特征

| 类别 | 代码 | 数量 |
|---|---|---:|
| 形态：面积或体积 | morphometry | 11 |
| 一阶统计：Mean、SD、P10、P50、P90、IQR | firstorder | 66 |
| 组成与分布：组织比值或占比，百分比 | composition | 12 |
| 复合：面积或体积×平均HU | composite | 11 |
| 合计 | | 100 |

旧脚本的8项骨参照比和8项骨参照比×HU不再混入基础输出。

组织代码：SM骨骼肌(1)、SF皮下脂肪(4)、IMF肌间脂肪(2)、VF其余内脏脂肪(3)、PVAT血管周围脂肪(6)、EAT心外膜脂肪(7)、AorticPVAT主动脉周围脂肪(8)、Bone骨(5)、CVF(6/7/8)、VF_total(3/6/7/8)、TF(2/3/4/6/7/8)。基础标签互斥。

## 0.1.4 自定义标签和缺失组织

相比0.1.3，本版保留`BodyCompositionExtractor(..., label_map=...)`的调用方式、特征名及缺失处理规则，仅将内部实现改为直接读取原始标签。数量从原始标签计数中查询；HU统计按原始标签筛选CT值。默认标签和自定义标签均不生成重编号mask，但HU筛选和数值统计仍可能产生临时数组。

`label_map`按以下固定顺序填写**使用者原始BC mask中的标签编号**，编号由使用者决定，不要求为1–8：

| 位置 | 库的标准标签 | 组织 |
|---|---|---|
| 1 | 1 | SM 骨骼肌 |
| 2 | 2 | IMF 肌间脂肪 |
| 3 | 3 | VF 其余内脏脂肪 |
| 4 | 4 | SF 皮下脂肪 |
| 5 | 5 | Bone 骨 |
| 6 | 6 | PVAT 血管周围脂肪 |
| 7 | 7 | EAT 心外膜脂肪 |
| 8 | 8 | AorticPVAT 主动脉周围脂肪 |

```python
from ctbodycomp import BodyCompositionExtractor
from ctbodycomp.batch import extract_csv

# 这些编号只是示例，请替换成自己的分割标签。
# 第三项为0表示没有VF标签，不能把背景0当成VF。
label_map = [2, 8, 0, 5, 4, 3, 6, 1]
extractor = BodyCompositionExtractor(mode="3D", label_map=label_map)
raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz")
# 同一个extractor可用于批量CSV，病例表格式不变：
# result = extract_csv("cases.csv", "features.csv", extractor)

# 2D有脊柱时，BC标签映射与脊柱标签分别设置：
extractor_2d = BodyCompositionExtractor(mode="2D", spine_label=7, label_map=label_map)
# raw_2d = extractor_2d.execute("ct.nii.gz", "bc_mask.nii.gz", spine_mask="spine.nii.gz")
```

不传`label_map`时沿用标准顺序`[1, 2, 3, 4, 5, 6, 7, 8]`。传入时必须为8项整数列表或元组；缺失组织填`0`，非零编号必须为正整数且不能重复。未映射的其他标签不参与组织计算。0.1.4按映射直接查询原始mask中的标签计数和对应CT值，不创建转换后的标签mask，不修改原mask，也不会修改脊柱标签或影像空间。整个批次使用同一映射，标签规范不同的病例应分批设置各自的映射。

**0.1.3改变了缺失组织的处理：**在实际选中的范围内先统计标签，所选特征的依赖标签全部存在才计算。缺失组织的面积、体积、一阶统计及原始复合均为`NaN`，CSV保存为空单元格；保留所有选定特征列，不按病例删除列。标签没有出现表示本次无法提供该组织的测量，不再用0表示缺失。

- CVF需要6、7、8全部存在；VF_total需要3、6、7、8全部存在；TF需要2、3、4、6、7、8全部存在。缺一个标签，对应复合组织的所有特征留空，不能用部分组织之和替代。
- 组成与分布特征检查分子、分母所依赖的全部标签。例如MFF需要1、2；SF_to_SM需要4、1；VF_total_to_SM需要3、6、7、8、1。缺少任意依赖就留空。
- 2D两层模式仍先逐层计算，再取等权平均；某特征在任一层缺少依赖时，最终该特征也留空。整幅影像其他层存在的标签不能补足当前测量范围的缺失。
- 后续校正保留这些缺失值：目标组织缺失时，其校正指数与校正复合留空；骨缺失时骨校正留空，但有有效身高等参数时其他校正仍可计算。特征值留空不等于病例提取失败，批量流程仍保留病例及状态。

`extractor.last_diagnostics`包含所用的`label_map`及选中范围出现的标准`present_labels`。它们记录映射和观察到的标签，不证明分割边界或组织定义与原项目相同。尤其VF需要排除单独分割的6–8区域，骨和脂肪组织的具体边界需符合相应输入规范；标签映射只更换编号。

## 先提取第一类，再校正

```python
from ctbodycomp import BodyCompositionExtractor, FeatureNormalizer

extractor = BodyCompositionExtractor(
    mode="3D",
    features={"morphometry": ["SM_Volume", "SF_Volume", "Bone_Volume"]},
)
raw = extractor.execute(image="ct.nii.gz", mask="bc_mask.nii.gz")

# 只校正指定组织；原始结果与校正结果是两个独立字典。
normalizer = FeatureNormalizer(
    mode="3D", methods=["height", "weight", "bsa", "bone"], tissues=["SM", "SF"]
)
corrected = normalizer.execute(raw, height_cm=170, weight_kg=65)
```

校正器根据第一类形态指标计算校正指数；开启`include_composite=True`时，同时读取一阶Mean并计算校正指数×Mean。不读取影像，不对HU本身或组成比值二次校正。可以把提取结果保存后，换一组校正方式再次计算。骨校正需提前提取同一范围的`Bone_Volume`或`Bone_Area`。

| 方法 | 公式 | 2D单位 | 3D单位 |
|---|---|---|---|
| height | 原始面积/体积 ÷ 身高(m)² | cm²/m² | cm³/m² |
| weight | 原始面积/体积 ÷ 体重(kg) | cm²/kg | cm³/kg |
| bsa | 原始面积/体积 ÷ BSA(m²) | cm²/m² | cm³/m² |
| bone | 原始面积/体积 ÷ 同范围骨面积/体积 | 无量纲 | 无量纲 |

2D和3D都使用身高²。默认BSA使用Mosteller公式：`sqrt(height_cm * weight_kg / 3600)`，也可通过`bsa_m2`直接提供已知BSA；提供值优先，来源记录在`normalizer.last_diagnostics`。Mosteller公式参考：[原始论文](https://doi.org/10.1056/NEJM198710223171717)。

身高、体重、BSA需为有限正数；只选择骨校正时不需要身高体重。缺少所选方法需要的参数会抛出明确错误。骨量为0时骨校正输出NaN。CSV校正失败时保留原始特征及失败原因。

若提取全部11个形态特征且启用全部校正，height、weight、bsa各11项，bone对10个非骨组织输出，共43项。与100项基础特征分开存储。骨校正包括CVF和TF两个聚合组织，不输出恒为1的Bone/Bone。实际输出量由所选组织、特征和校正方式决定。

## 可选：校正指数 × 平均HU

从0.1.1开始，校正指数与校正后的复合特征统一由`FeatureNormalizer`管理。使用`include_composite=True`，同一次校正返回“指数”和“指数×同组织平均HU”，每一对相邻排列。两者均属于校正模块，与四类基础特征分开。默认不开启校正复合项。

```python
from ctbodycomp import BodyCompositionExtractor, FeatureNormalizer

extractor = BodyCompositionExtractor(
    mode="3D",
    features={
        "morphometry": ["SM_Volume", "Bone_Volume"],
        "firstorder": ["SM_Mean"],
    },
)
raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz")
normalizer = FeatureNormalizer(
    mode="3D", methods=["height", "bone"], tissues=["SM"],
    include_composite=True,
)
corrected = normalizer.execute(raw, height_cm=170)
# normalization_height_SM_Volume
# normalized_composite_height_SM_VolumeMean
# normalization_bone_SM_Volume
# normalized_composite_bone_SM_VolumeMean
units = normalizer.get_units(raw)
```

2D将模式改为`2D`，将第一类特征的`Volume`改为`Area`，输出为`normalized_composite_height_SM_AreaMean`等。

| 方法 | 公式，μ为同组织平均HU | 2D单位 | 3D单位 | 全部组织数量 |
|---|---|---|---|---:|
| height | (面积/体积 ÷ 身高(m)²) × μ | cm²·HU/m² | cm³·HU/m² | 11 |
| weight | (面积/体积 ÷ 体重(kg)) × μ | cm²·HU/kg | cm³·HU/kg | 11 |
| bsa | (面积/体积 ÷ BSA(m²)) × μ | cm²·HU/m² | cm³·HU/m² | 11 |
| bone | (面积/体积 ÷ 同范围骨面积/体积) × μ | HU | HU | 10 |

共43项可选校正复合特征，不输出Bone/Bone×HU。每种模式最多有100项基础特征＋43项校正指数＋43项校正复合特征＝186项。2D和3D是两种模式，不合并计数。负平均HU保留符号；指数或均值为NaN/无穷时结果为NaN，不解释为实际组织质量。

通过`methods`选择校正方式，通过`tissues`选择组织；`tissues=None`从输入已有的形态指标中选择组织。开启复合项需要同时提取目标组织的Mean。可用`get_normalized_composite_catalog("3D")`查询完整复合名称、单位、指数键和均值键。

对于已经保存的校正指数字典，仍支持`NormalizedCompositeCalculator(mode, methods, tissues).execute(indices, raw)`单独补算复合项；两个字典需来自同一病例、同一范围。

**2D两层的计算顺序**：先将两层面积与两层平均HU分别等权平均，面积平均值进行校正，再乘HU平均值。因此结果是“校正后的平均面积指数 × 平均HU”，与基础复合类的“逐层面积×HU后平均”定义不同。例如两层面积0.06、0.18 cm²，均值10、30 HU，身高1 m：校正复合为0.12×20＝2.4 cm²·HU/m²，而基础复合为(0.06×10＋0.18×30)/2＝3 cm²·HU。

## 选择类别或单项

```python
from ctbodycomp import BodyCompositionExtractor, get_feature_catalog

extractor = BodyCompositionExtractor(mode="2D", spine_label=7)
extractor.disableAllFeatures()
extractor.enableFeaturesByName(
    morphometry=["SM_Area", "Bone_Area"],
    firstorder=["SM_Mean", "SM_P50"],
    composition=["MFF"],
)
raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz", spine_mask="spine.nii.gz")

# 按整个类别启用：
extractor.enableFeatureClassByName("firstorder")
# 查询当前模式下全部可用名称、英文输出键和单位：
catalog = get_feature_catalog("2D")
```

构造参数`features=None`默认启用全部四类；`features={}`不启用任何特征。映射中未列出的类别不提取，类别中的`[]`或`None`表示该类全部。`enableFeaturesByName`替换指定类别的选择，其他已启用类别保持不变，因此要只选择几项时先调用`disableAllFeatures()`。支持全部启用`enableAllFeatures()`和单类别关闭`enableFeatureClassByName("firstorder", False)`。

只输出选择的特征，内部依赖按需计算。例如只选Mean不计算分位数，只选形态不计算HU统计；只选一个组成比值时也不输出其内部依赖的组织面积/体积。

## 输入与选层

- 必需：CT与BC mask。接受文件路径或nibabel图像对象，图像需3D且空间匹配，尺寸以mm解释。
- 裁剪、选层和特征计算前，统一检查CT与BC mask的完整数组尺寸（shape）及空间变换矩阵（affine）；提供Spine mask时也检查它与CT一致。affine包含体素尺寸、方向和原点，比较使用绝对容差`1e-4`、相对容差`0`。不一致直接失败，不自动重采样或对齐。CSV批量提取会记录`extraction_status=failed`和具体原因，并继续其他病例。
- 可选：Spine mask。不提供时3D计算整幅CT，2D在整幅CT全部层中选中间层。
- 2D有Spine mask时需`spine_label`，例如7或8。先找到该标签出现的最小、最大切片，以首末之间的整个连续区间为候选范围；奇数层取中间一层，偶数层取中间两层，即使中间层没有该脊柱标签也照常使用。
- 3D有Spine mask时默认范围`spine_range=(7, 18)`，可配置，取首末层之间的完整范围。新库依据affine识别上下方向，支持数组轴置换。
- 每个选中切片的组织ROI来自完整BC mask平面，Spine mask仅定位。斜位影像保持原切片平面。
- 2D两层基础特征先逐层计算再等权平均。独立校正使用这个平均后的形态值；骨校正因此是“平均组织面积/平均骨面积”，不同于旧脚本的“两层骨比值平均”。
- SD使用ddof=0；分位数为线性插值；非有限CT值不进入HU统计；缺失组织的面积/体积及其他特征均为NaN；存在组织但没有有限HU样本时，数量可计算而HU统计与乘HU项为NaN；任一层某基础指标NaN，则两层平均也为NaN。

CSV输出顺序为：原输入信息列 → `diagnostics_scope`、`diagnostics_slices`、`extraction_status` → 所选特征列 → `extraction_error`。连续切片简写为`42-374`，单层为`42`；二维中间两层同样表示为`42-43`。`extractor.last_diagnostics["selected_slices"]`仍保留完整列表，只有CSV显示采用简写。

诊断信息在`extractor.last_diagnostics`，与纯数值特征分开。无Spine mask请传`None`；填写了错误文件路径不会自动退回全图计算。

## CSV分两步运行

新库的CSV使用有意义的列名（区分大小写），不同于旧脚本按列位置读取：

```csv
ID,Image,Mask,SpineMask,Height_cm,Weight_kg
001,ct.nii.gz,bc_mask.nii.gz,,170,65
```

必需`Image`、`Mask`，其他列可选；`SpineMask`可省略或留空。相对路径相对于输入CSV目录。`Height_cm`、`Weight_kg`、可选`BSA_m2`由校正步骤读取，逐病例值优先于命令行公共值。

```bash
# 查询特征名
ctbodycomp features --mode 3D

# 仅提取肌肉、皮下脂肪和骨的体积
ctbodycomp extract cases.csv raw.csv --mode 3D --config examples/morphometry_3d.json

# 对提取结果进行校正，原始列也保留在输出中
ctbodycomp normalize raw.csv corrected.csv --mode 3D --methods height weight bsa bone --tissues SM SF

# 2D单标签、部分特征
ctbodycomp extract cases.csv raw_2d.csv --mode 2D --label 7 --config examples/selected_2d.json
ctbodycomp normalize raw_2d.csv corrected_2d.csv --mode 2D --methods bone --tissues SM

# 自定义BC标签：按SM、IMF、VF、SF、Bone、PVAT、EAT、AorticPVAT顺序；0表示缺失
ctbodycomp extract cases.csv raw_mapped.csv --mode 3D --label-map 2 8 0 5 4 3 6 1
```

需要校正复合时，提取配置必须包含目标组织的`firstorder_*_Mean`。在同一校正步骤增加`--include-composite`即可：

```bash
# 示例配置包含肌肉、骨体积和肌肉Mean
ctbodycomp extract cases.csv raw.csv --mode 3D --config examples/normalized_composite_3d.json
ctbodycomp normalize raw.csv corrected.csv --mode 3D --methods height weight bsa bone --tissues SM --include-composite

# 查询校正复合特征及所需输入
ctbodycomp features --mode 3D --normalized-composite
```

校正CSV保留基础特征，按“校正指数、对应复合项”的顺序追加列，使用统一的`normalization_status`、`normalization_error`记录结果。缺少所需Mean列时记录失败；Mean数值为空时对应复合项为NaN。影像不存在也不影响已成功提取结果的后续计算。

如需对已有校正CSV补算复合项，也保留单独命令：`ctbodycomp normalized-composite corrected.csv products.csv --mode 3D --methods height bone --tissues SM`。

配置JSON直接写“类别→特征名称列表”。命令返回0表示成功，1表示输出中存在失败病例，参数错误返回2。输出目录需存在，输出不能覆盖输入文件；重复输出路径会替换先前结果。

## 完整使用示例

源码包中的 `examples/reference_usage/README.md` 提供五种完整提取示例：2D T12、2D无脊柱、3D T12、3D T1–T12、3D无脊柱，以及独立校正示例。每种情况同时提供可运行的Python脚本和Markdown教程，不依赖额外本地辅助脚本。

输入使用 `cases.example.csv` 和 `cases_without_spine.example.csv` 通用模板。模板不包含实际病例数据，使用前替换为自己的影像路径。示例计算结果写入示例目录下的 `results/`，不会纳入发布包。

## 测试和构建

在项目根目录运行：

```bash
python -m pip install -e '.[test]'
python -m pytest -q -p no:cacheprovider
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/ctbodycomp-0.1.4-py3-none-any.whl dist/ctbodycomp-0.1.4.tar.gz
```

wheel包含库模块、许可证和安装元数据；源码包另外包含测试与使用教程。虚拟环境、缓存、本地影像、实际病例表和计算结果不纳入发布文件。`PUBLISHING.md` 仅保留在开发目录供维护者参考，不随发布包分发。

## 许可证

本版本采用 [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0)，完整原文随发布包附于 `LICENSE`。该许可证不包含两年后自动转换为MIT的条款；不在其许可范围内的商业用途需版权持有人另行授权。

本许可允许在许可范围内修改和分发代码，未设“禁止修改”限制。教育机构、公共研究机构、公共卫生或健康组织等的使用，被其机构条款明确列为允许用途，不因资金来源或相关资金义务改变；不能把本许可理解为所有医院收费场景都必须另行申请。实际允许范围以完整原文为准。

本项目为公开源码、限制用途的软件库，不声明为OSI认可的开源许可证。作者姓名及邮箱元数据仍未填写。
