ctbodycomp 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. ctbodycomp-0.1.1/.gitignore +17 -0
  2. ctbodycomp-0.1.1/MANIFEST.in +18 -0
  3. ctbodycomp-0.1.1/PKG-INFO +216 -0
  4. ctbodycomp-0.1.1/README.md +200 -0
  5. ctbodycomp-0.1.1/examples/morphometry_3d.json +3 -0
  6. ctbodycomp-0.1.1/examples/normalized_composite_3d.json +4 -0
  7. ctbodycomp-0.1.1/examples/reference_usage/01_2d_t12.md +107 -0
  8. ctbodycomp-0.1.1/examples/reference_usage/01_2d_t12.py +75 -0
  9. ctbodycomp-0.1.1/examples/reference_usage/02_2d_without_spine.md +107 -0
  10. ctbodycomp-0.1.1/examples/reference_usage/02_2d_without_spine.py +75 -0
  11. ctbodycomp-0.1.1/examples/reference_usage/03_3d_t12.md +107 -0
  12. ctbodycomp-0.1.1/examples/reference_usage/03_3d_t12.py +75 -0
  13. ctbodycomp-0.1.1/examples/reference_usage/04_3d_t1_t12.md +107 -0
  14. ctbodycomp-0.1.1/examples/reference_usage/04_3d_t1_t12.py +75 -0
  15. ctbodycomp-0.1.1/examples/reference_usage/05_3d_without_spine.md +107 -0
  16. ctbodycomp-0.1.1/examples/reference_usage/05_3d_without_spine.py +75 -0
  17. ctbodycomp-0.1.1/examples/reference_usage/06_/346/217/220/345/217/226/345/220/216/346/240/241/346/255/243.md +140 -0
  18. ctbodycomp-0.1.1/examples/reference_usage/06_/346/217/220/345/217/226/345/220/216/346/240/241/346/255/243.py +27 -0
  19. ctbodycomp-0.1.1/examples/reference_usage/README.md +174 -0
  20. ctbodycomp-0.1.1/examples/reference_usage/cases.example.csv +2 -0
  21. ctbodycomp-0.1.1/examples/reference_usage/cases_without_spine.example.csv +2 -0
  22. ctbodycomp-0.1.1/examples/selected_2d.json +5 -0
  23. ctbodycomp-0.1.1/pyproject.toml +35 -0
  24. ctbodycomp-0.1.1/setup.cfg +4 -0
  25. ctbodycomp-0.1.1/src/ctbodycomp/__init__.py +9 -0
  26. ctbodycomp-0.1.1/src/ctbodycomp/batch.py +132 -0
  27. ctbodycomp-0.1.1/src/ctbodycomp/cli.py +72 -0
  28. ctbodycomp-0.1.1/src/ctbodycomp/definitions.py +61 -0
  29. ctbodycomp-0.1.1/src/ctbodycomp/extractor.py +84 -0
  30. ctbodycomp-0.1.1/src/ctbodycomp/features.py +71 -0
  31. ctbodycomp-0.1.1/src/ctbodycomp/image.py +74 -0
  32. ctbodycomp-0.1.1/src/ctbodycomp/normalization.py +131 -0
  33. ctbodycomp-0.1.1/src/ctbodycomp/normalized_composite.py +88 -0
  34. ctbodycomp-0.1.1/src/ctbodycomp.egg-info/PKG-INFO +216 -0
  35. ctbodycomp-0.1.1/src/ctbodycomp.egg-info/SOURCES.txt +39 -0
  36. ctbodycomp-0.1.1/src/ctbodycomp.egg-info/dependency_links.txt +1 -0
  37. ctbodycomp-0.1.1/src/ctbodycomp.egg-info/entry_points.txt +2 -0
  38. ctbodycomp-0.1.1/src/ctbodycomp.egg-info/requires.txt +6 -0
  39. ctbodycomp-0.1.1/src/ctbodycomp.egg-info/top_level.txt +1 -0
  40. ctbodycomp-0.1.1/tests/test_normalized_composite.py +194 -0
  41. ctbodycomp-0.1.1/tests/test_package.py +199 -0
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .venv*/
8
+ examples/reference_usage/results/
9
+ examples/reference_usage/md_results/
10
+ examples/reference_usage/reference_results/
11
+ examples/reference_usage/data/
12
+ examples/reference_usage/*_cases.csv
13
+ *.nii
14
+ *.nii.gz
15
+ *.nrrd
16
+ *.mha
17
+ *.mhd
@@ -0,0 +1,18 @@
1
+ include README.md
2
+ exclude PUBLISHING.md
3
+ include pyproject.toml
4
+ include .gitignore
5
+ recursive-include src/ctbodycomp *.py
6
+ recursive-include tests *.py
7
+ recursive-include examples *.py *.json *.md
8
+ include examples/reference_usage/cases.example.csv
9
+ include examples/reference_usage/cases_without_spine.example.csv
10
+ prune build
11
+ prune dist
12
+ prune .venv*
13
+ prune .pytest_cache
14
+ prune examples/reference_usage/results
15
+ prune examples/reference_usage/md_results
16
+ prune examples/reference_usage/reference_results
17
+ prune examples/reference_usage/data
18
+ global-exclude __pycache__ *.py[cod] *.nii *.nii.gz *.nrrd *.mha *.mhd
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctbodycomp
3
+ Version: 0.1.1
4
+ Summary: Selectable 2D and 3D CT body composition features with separate normalization
5
+ Keywords: CT,body-composition,medical-imaging,feature-extraction
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: numpy>=1.22
12
+ Requires-Dist: nibabel>=5
13
+ Requires-Dist: pandas>=2
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=7; extra == "test"
16
+
17
+ # ctbodycomp
18
+
19
+ CT体成分2D/3D特征提取与独立校正。支持按类别或单个名称选择基础特征,以及独立的身高、体重、BSA和骨参照校正。
20
+
21
+ ## 安装
22
+
23
+ 在本项目目录运行`python -m pip install .`。开发时使用`python -m pip install -e .`。
24
+ 支持Python 3.10及以上,依赖numpy、nibabel、pandas;测试需要pytest。
25
+
26
+ ## 四类基础特征
27
+
28
+ | 类别 | 代码 | 数量 |
29
+ |---|---|---:|
30
+ | 形态:面积或体积 | morphometry | 11 |
31
+ | 一阶统计:Mean、SD、P10、P50、P90、IQR | firstorder | 66 |
32
+ | 组成与分布:组织比值或占比,百分比 | composition | 12 |
33
+ | 复合:面积或体积×平均HU | composite | 11 |
34
+ | 合计 | | 100 |
35
+
36
+ 旧脚本的8项骨参照比和8项骨参照比×HU不再混入基础输出。
37
+
38
+ 组织代码: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)。基础标签互斥。
39
+
40
+ ## 先提取第一类,再校正
41
+
42
+ ```python
43
+ from ctbodycomp import BodyCompositionExtractor, FeatureNormalizer
44
+
45
+ extractor = BodyCompositionExtractor(
46
+ mode="3D",
47
+ features={"morphometry": ["SM_Volume", "SF_Volume", "Bone_Volume"]},
48
+ )
49
+ raw = extractor.execute(image="ct.nii.gz", mask="bc_mask.nii.gz")
50
+
51
+ # 只校正指定组织;原始结果与校正结果是两个独立字典。
52
+ normalizer = FeatureNormalizer(
53
+ mode="3D", methods=["height", "weight", "bsa", "bone"], tissues=["SM", "SF"]
54
+ )
55
+ corrected = normalizer.execute(raw, height_cm=170, weight_kg=65)
56
+ ```
57
+
58
+ 校正器根据第一类形态指标计算校正指数;开启`include_composite=True`时,同时读取一阶Mean并计算校正指数×Mean。不读取影像,不对HU本身或组成比值二次校正。可以把提取结果保存后,换一组校正方式再次计算。骨校正需提前提取同一范围的`Bone_Volume`或`Bone_Area`。
59
+
60
+ | 方法 | 公式 | 2D单位 | 3D单位 |
61
+ |---|---|---|---|
62
+ | height | 原始面积/体积 ÷ 身高(m)² | cm²/m² | cm³/m² |
63
+ | weight | 原始面积/体积 ÷ 体重(kg) | cm²/kg | cm³/kg |
64
+ | bsa | 原始面积/体积 ÷ BSA(m²) | cm²/m² | cm³/m² |
65
+ | bone | 原始面积/体积 ÷ 同范围骨面积/体积 | 无量纲 | 无量纲 |
66
+
67
+ 2D和3D都使用身高²。默认BSA使用Mosteller公式:`sqrt(height_cm * weight_kg / 3600)`,也可通过`bsa_m2`直接提供已知BSA;提供值优先,来源记录在`normalizer.last_diagnostics`。Mosteller公式参考:[原始论文](https://doi.org/10.1056/NEJM198710223171717)。
68
+
69
+ 身高、体重、BSA需为有限正数;只选择骨校正时不需要身高体重。缺少所选方法需要的参数会抛出明确错误。骨量为0时骨校正输出NaN。CSV校正失败时保留原始特征及失败原因。
70
+
71
+ 若提取全部11个形态特征且启用全部校正,height、weight、bsa各11项,bone对10个非骨组织输出,共43项。与100项基础特征分开存储。骨校正包括CVF和TF两个聚合组织,不输出恒为1的Bone/Bone。实际输出量由所选组织、特征和校正方式决定。
72
+
73
+ ## 可选:校正指数 × 平均HU
74
+
75
+ 从0.1.1开始,校正指数与校正后的复合特征统一由`FeatureNormalizer`管理。使用`include_composite=True`,同一次校正返回“指数”和“指数×同组织平均HU”,每一对相邻排列。两者均属于校正模块,与四类基础特征分开。默认不开启校正复合项。
76
+
77
+ ```python
78
+ from ctbodycomp import BodyCompositionExtractor, FeatureNormalizer
79
+
80
+ extractor = BodyCompositionExtractor(
81
+ mode="3D",
82
+ features={
83
+ "morphometry": ["SM_Volume", "Bone_Volume"],
84
+ "firstorder": ["SM_Mean"],
85
+ },
86
+ )
87
+ raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz")
88
+ normalizer = FeatureNormalizer(
89
+ mode="3D", methods=["height", "bone"], tissues=["SM"],
90
+ include_composite=True,
91
+ )
92
+ corrected = normalizer.execute(raw, height_cm=170)
93
+ # normalization_height_SM_Volume
94
+ # normalized_composite_height_SM_VolumeMean
95
+ # normalization_bone_SM_Volume
96
+ # normalized_composite_bone_SM_VolumeMean
97
+ units = normalizer.get_units(raw)
98
+ ```
99
+
100
+ 2D将模式改为`2D`,将第一类特征的`Volume`改为`Area`,输出为`normalized_composite_height_SM_AreaMean`等。
101
+
102
+ | 方法 | 公式,μ为同组织平均HU | 2D单位 | 3D单位 | 全部组织数量 |
103
+ |---|---|---|---|---:|
104
+ | height | (面积/体积 ÷ 身高(m)²) × μ | cm²·HU/m² | cm³·HU/m² | 11 |
105
+ | weight | (面积/体积 ÷ 体重(kg)) × μ | cm²·HU/kg | cm³·HU/kg | 11 |
106
+ | bsa | (面积/体积 ÷ BSA(m²)) × μ | cm²·HU/m² | cm³·HU/m² | 11 |
107
+ | bone | (面积/体积 ÷ 同范围骨面积/体积) × μ | HU | HU | 10 |
108
+
109
+ 共43项可选校正复合特征,不输出Bone/Bone×HU。每种模式最多有100项基础特征+43项校正指数+43项校正复合特征=186项。2D和3D是两种模式,不合并计数。负平均HU保留符号;指数或均值为NaN/无穷时结果为NaN,不解释为实际组织质量。
110
+
111
+ 通过`methods`选择校正方式,通过`tissues`选择组织;`tissues=None`从输入已有的形态指标中选择组织。开启复合项需要同时提取目标组织的Mean。可用`get_normalized_composite_catalog("3D")`查询完整复合名称、单位、指数键和均值键。
112
+
113
+ 对于已经保存的校正指数字典,仍支持`NormalizedCompositeCalculator(mode, methods, tissues).execute(indices, raw)`单独补算复合项;两个字典需来自同一病例、同一范围。
114
+
115
+ **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。
116
+
117
+ ## 选择类别或单项
118
+
119
+ ```python
120
+ from ctbodycomp import BodyCompositionExtractor, get_feature_catalog
121
+
122
+ extractor = BodyCompositionExtractor(mode="2D", spine_label=7)
123
+ extractor.disableAllFeatures()
124
+ extractor.enableFeaturesByName(
125
+ morphometry=["SM_Area", "Bone_Area"],
126
+ firstorder=["SM_Mean", "SM_P50"],
127
+ composition=["MFF"],
128
+ )
129
+ raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz", spine_mask="spine.nii.gz")
130
+
131
+ # 按整个类别启用:
132
+ extractor.enableFeatureClassByName("firstorder")
133
+ # 查询当前模式下全部可用名称、英文输出键和单位:
134
+ catalog = get_feature_catalog("2D")
135
+ ```
136
+
137
+ 构造参数`features=None`默认启用全部四类;`features={}`不启用任何特征。映射中未列出的类别不提取,类别中的`[]`或`None`表示该类全部。`enableFeaturesByName`替换指定类别的选择,其他已启用类别保持不变,因此要只选择几项时先调用`disableAllFeatures()`。支持全部启用`enableAllFeatures()`和单类别关闭`enableFeatureClassByName("firstorder", False)`。
138
+
139
+ 只输出选择的特征,内部依赖按需计算。例如只选Mean不计算分位数,只选形态不计算HU统计;只选一个组成比值时也不输出其内部依赖的组织面积/体积。
140
+
141
+ ## 输入与选层
142
+
143
+ - 必需:CT与BC mask。接受文件路径或nibabel图像对象,图像需3D且空间匹配,尺寸以mm解释;不重采样。
144
+ - 可选:Spine mask。不提供时3D计算整幅CT,2D在整幅CT全部层中选中间层。
145
+ - 2D有Spine mask时需`spine_label`,例如7或8。仅对该标签实际出现的切片排序,奇数取中间一层,偶数取中间两层。
146
+ - 3D有Spine mask时默认范围`spine_range=(7, 18)`,可配置,取首末层之间的完整范围。新库依据affine识别上下方向,支持数组轴置换。
147
+ - 每个选中切片的组织ROI来自完整BC mask平面,Spine mask仅定位。斜位影像保持原切片平面。
148
+ - 2D两层基础特征先逐层计算再等权平均。独立校正使用这个平均后的形态值;骨校正因此是“平均组织面积/平均骨面积”,不同于旧脚本的“两层骨比值平均”。
149
+ - SD使用ddof=0;分位数为线性插值;非有限CT值不进入HU统计;空组织的面积/体积为0,HU统计为NaN;任一层某基础指标NaN,则两层平均也为NaN。
150
+
151
+ 诊断信息在`extractor.last_diagnostics`,与纯数值特征分开。无Spine mask请传`None`;填写了错误文件路径不会自动退回全图计算。
152
+
153
+ ## CSV分两步运行
154
+
155
+ 新库的CSV使用有意义的列名(区分大小写),不同于旧脚本按列位置读取:
156
+
157
+ ```csv
158
+ ID,Image,Mask,SpineMask,Height_cm,Weight_kg
159
+ 001,ct.nii.gz,bc_mask.nii.gz,,170,65
160
+ ```
161
+
162
+ 必需`Image`、`Mask`,其他列可选;`SpineMask`可省略或留空。相对路径相对于输入CSV目录。`Height_cm`、`Weight_kg`、可选`BSA_m2`由校正步骤读取,逐病例值优先于命令行公共值。
163
+
164
+ ```bash
165
+ # 查询特征名
166
+ ctbodycomp features --mode 3D
167
+
168
+ # 仅提取肌肉、皮下脂肪和骨的体积
169
+ ctbodycomp extract cases.csv raw.csv --mode 3D --config examples/morphometry_3d.json
170
+
171
+ # 对提取结果进行校正,原始列也保留在输出中
172
+ ctbodycomp normalize raw.csv corrected.csv --mode 3D --methods height weight bsa bone --tissues SM SF
173
+
174
+ # 2D单标签、部分特征
175
+ ctbodycomp extract cases.csv raw_2d.csv --mode 2D --label 7 --config examples/selected_2d.json
176
+ ctbodycomp normalize raw_2d.csv corrected_2d.csv --mode 2D --methods bone --tissues SM
177
+ ```
178
+
179
+ 需要校正复合时,提取配置必须包含目标组织的`firstorder_*_Mean`。在同一校正步骤增加`--include-composite`即可:
180
+
181
+ ```bash
182
+ # 示例配置包含肌肉、骨体积和肌肉Mean
183
+ ctbodycomp extract cases.csv raw.csv --mode 3D --config examples/normalized_composite_3d.json
184
+ ctbodycomp normalize raw.csv corrected.csv --mode 3D --methods height weight bsa bone --tissues SM --include-composite
185
+
186
+ # 查询校正复合特征及所需输入
187
+ ctbodycomp features --mode 3D --normalized-composite
188
+ ```
189
+
190
+ 校正CSV保留基础特征,按“校正指数、对应复合项”的顺序追加列,使用统一的`normalization_status`、`normalization_error`记录结果。缺少所需Mean列时记录失败;Mean数值为空时对应复合项为NaN。影像不存在也不影响已成功提取结果的后续计算。
191
+
192
+ 如需对已有校正CSV补算复合项,也保留单独命令:`ctbodycomp normalized-composite corrected.csv products.csv --mode 3D --methods height bone --tissues SM`。
193
+
194
+ 配置JSON直接写“类别→特征名称列表”。命令返回0表示成功,1表示输出中存在失败病例,参数错误返回2。输出目录需存在,输出不能覆盖输入文件;重复输出路径会替换先前结果。
195
+
196
+ ## 完整使用示例
197
+
198
+ 源码包中的 `examples/reference_usage/README.md` 提供五种完整提取示例:2D T12、2D无脊柱、3D T12、3D T1–T12、3D无脊柱,以及独立校正示例。每种情况同时提供可运行的Python脚本和Markdown教程,不依赖额外本地辅助脚本。
199
+
200
+ 输入使用 `cases.example.csv` 和 `cases_without_spine.example.csv` 通用模板。模板不包含实际病例数据,使用前替换为自己的影像路径。示例计算结果写入示例目录下的 `results/`,不会纳入发布包。
201
+
202
+ ## 测试和构建
203
+
204
+ 在项目根目录运行:
205
+
206
+ ```bash
207
+ python -m pip install -e '.[test]'
208
+ python -m pytest -q -p no:cacheprovider
209
+ python -m pip install --upgrade build twine
210
+ python -m build
211
+ python -m twine check dist/ctbodycomp-0.1.1-py3-none-any.whl dist/ctbodycomp-0.1.1.tar.gz
212
+ ```
213
+
214
+ wheel只包含库模块和安装元数据;源码包另外包含测试与使用教程。虚拟环境、缓存、本地影像、实际病例表和计算结果不纳入发布文件。`PUBLISHING.md` 仅保留在开发目录供维护者参考,不随发布包分发。
215
+
216
+ 作者、邮箱和许可证字段当前未设置,可在后续版本补充。本项目目前没有声明MIT或Apache-2.0授权。
@@ -0,0 +1,200 @@
1
+ # ctbodycomp
2
+
3
+ CT体成分2D/3D特征提取与独立校正。支持按类别或单个名称选择基础特征,以及独立的身高、体重、BSA和骨参照校正。
4
+
5
+ ## 安装
6
+
7
+ 在本项目目录运行`python -m pip install .`。开发时使用`python -m pip install -e .`。
8
+ 支持Python 3.10及以上,依赖numpy、nibabel、pandas;测试需要pytest。
9
+
10
+ ## 四类基础特征
11
+
12
+ | 类别 | 代码 | 数量 |
13
+ |---|---|---:|
14
+ | 形态:面积或体积 | morphometry | 11 |
15
+ | 一阶统计:Mean、SD、P10、P50、P90、IQR | firstorder | 66 |
16
+ | 组成与分布:组织比值或占比,百分比 | composition | 12 |
17
+ | 复合:面积或体积×平均HU | composite | 11 |
18
+ | 合计 | | 100 |
19
+
20
+ 旧脚本的8项骨参照比和8项骨参照比×HU不再混入基础输出。
21
+
22
+ 组织代码: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)。基础标签互斥。
23
+
24
+ ## 先提取第一类,再校正
25
+
26
+ ```python
27
+ from ctbodycomp import BodyCompositionExtractor, FeatureNormalizer
28
+
29
+ extractor = BodyCompositionExtractor(
30
+ mode="3D",
31
+ features={"morphometry": ["SM_Volume", "SF_Volume", "Bone_Volume"]},
32
+ )
33
+ raw = extractor.execute(image="ct.nii.gz", mask="bc_mask.nii.gz")
34
+
35
+ # 只校正指定组织;原始结果与校正结果是两个独立字典。
36
+ normalizer = FeatureNormalizer(
37
+ mode="3D", methods=["height", "weight", "bsa", "bone"], tissues=["SM", "SF"]
38
+ )
39
+ corrected = normalizer.execute(raw, height_cm=170, weight_kg=65)
40
+ ```
41
+
42
+ 校正器根据第一类形态指标计算校正指数;开启`include_composite=True`时,同时读取一阶Mean并计算校正指数×Mean。不读取影像,不对HU本身或组成比值二次校正。可以把提取结果保存后,换一组校正方式再次计算。骨校正需提前提取同一范围的`Bone_Volume`或`Bone_Area`。
43
+
44
+ | 方法 | 公式 | 2D单位 | 3D单位 |
45
+ |---|---|---|---|
46
+ | height | 原始面积/体积 ÷ 身高(m)² | cm²/m² | cm³/m² |
47
+ | weight | 原始面积/体积 ÷ 体重(kg) | cm²/kg | cm³/kg |
48
+ | bsa | 原始面积/体积 ÷ BSA(m²) | cm²/m² | cm³/m² |
49
+ | bone | 原始面积/体积 ÷ 同范围骨面积/体积 | 无量纲 | 无量纲 |
50
+
51
+ 2D和3D都使用身高²。默认BSA使用Mosteller公式:`sqrt(height_cm * weight_kg / 3600)`,也可通过`bsa_m2`直接提供已知BSA;提供值优先,来源记录在`normalizer.last_diagnostics`。Mosteller公式参考:[原始论文](https://doi.org/10.1056/NEJM198710223171717)。
52
+
53
+ 身高、体重、BSA需为有限正数;只选择骨校正时不需要身高体重。缺少所选方法需要的参数会抛出明确错误。骨量为0时骨校正输出NaN。CSV校正失败时保留原始特征及失败原因。
54
+
55
+ 若提取全部11个形态特征且启用全部校正,height、weight、bsa各11项,bone对10个非骨组织输出,共43项。与100项基础特征分开存储。骨校正包括CVF和TF两个聚合组织,不输出恒为1的Bone/Bone。实际输出量由所选组织、特征和校正方式决定。
56
+
57
+ ## 可选:校正指数 × 平均HU
58
+
59
+ 从0.1.1开始,校正指数与校正后的复合特征统一由`FeatureNormalizer`管理。使用`include_composite=True`,同一次校正返回“指数”和“指数×同组织平均HU”,每一对相邻排列。两者均属于校正模块,与四类基础特征分开。默认不开启校正复合项。
60
+
61
+ ```python
62
+ from ctbodycomp import BodyCompositionExtractor, FeatureNormalizer
63
+
64
+ extractor = BodyCompositionExtractor(
65
+ mode="3D",
66
+ features={
67
+ "morphometry": ["SM_Volume", "Bone_Volume"],
68
+ "firstorder": ["SM_Mean"],
69
+ },
70
+ )
71
+ raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz")
72
+ normalizer = FeatureNormalizer(
73
+ mode="3D", methods=["height", "bone"], tissues=["SM"],
74
+ include_composite=True,
75
+ )
76
+ corrected = normalizer.execute(raw, height_cm=170)
77
+ # normalization_height_SM_Volume
78
+ # normalized_composite_height_SM_VolumeMean
79
+ # normalization_bone_SM_Volume
80
+ # normalized_composite_bone_SM_VolumeMean
81
+ units = normalizer.get_units(raw)
82
+ ```
83
+
84
+ 2D将模式改为`2D`,将第一类特征的`Volume`改为`Area`,输出为`normalized_composite_height_SM_AreaMean`等。
85
+
86
+ | 方法 | 公式,μ为同组织平均HU | 2D单位 | 3D单位 | 全部组织数量 |
87
+ |---|---|---|---|---:|
88
+ | height | (面积/体积 ÷ 身高(m)²) × μ | cm²·HU/m² | cm³·HU/m² | 11 |
89
+ | weight | (面积/体积 ÷ 体重(kg)) × μ | cm²·HU/kg | cm³·HU/kg | 11 |
90
+ | bsa | (面积/体积 ÷ BSA(m²)) × μ | cm²·HU/m² | cm³·HU/m² | 11 |
91
+ | bone | (面积/体积 ÷ 同范围骨面积/体积) × μ | HU | HU | 10 |
92
+
93
+ 共43项可选校正复合特征,不输出Bone/Bone×HU。每种模式最多有100项基础特征+43项校正指数+43项校正复合特征=186项。2D和3D是两种模式,不合并计数。负平均HU保留符号;指数或均值为NaN/无穷时结果为NaN,不解释为实际组织质量。
94
+
95
+ 通过`methods`选择校正方式,通过`tissues`选择组织;`tissues=None`从输入已有的形态指标中选择组织。开启复合项需要同时提取目标组织的Mean。可用`get_normalized_composite_catalog("3D")`查询完整复合名称、单位、指数键和均值键。
96
+
97
+ 对于已经保存的校正指数字典,仍支持`NormalizedCompositeCalculator(mode, methods, tissues).execute(indices, raw)`单独补算复合项;两个字典需来自同一病例、同一范围。
98
+
99
+ **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。
100
+
101
+ ## 选择类别或单项
102
+
103
+ ```python
104
+ from ctbodycomp import BodyCompositionExtractor, get_feature_catalog
105
+
106
+ extractor = BodyCompositionExtractor(mode="2D", spine_label=7)
107
+ extractor.disableAllFeatures()
108
+ extractor.enableFeaturesByName(
109
+ morphometry=["SM_Area", "Bone_Area"],
110
+ firstorder=["SM_Mean", "SM_P50"],
111
+ composition=["MFF"],
112
+ )
113
+ raw = extractor.execute("ct.nii.gz", "bc_mask.nii.gz", spine_mask="spine.nii.gz")
114
+
115
+ # 按整个类别启用:
116
+ extractor.enableFeatureClassByName("firstorder")
117
+ # 查询当前模式下全部可用名称、英文输出键和单位:
118
+ catalog = get_feature_catalog("2D")
119
+ ```
120
+
121
+ 构造参数`features=None`默认启用全部四类;`features={}`不启用任何特征。映射中未列出的类别不提取,类别中的`[]`或`None`表示该类全部。`enableFeaturesByName`替换指定类别的选择,其他已启用类别保持不变,因此要只选择几项时先调用`disableAllFeatures()`。支持全部启用`enableAllFeatures()`和单类别关闭`enableFeatureClassByName("firstorder", False)`。
122
+
123
+ 只输出选择的特征,内部依赖按需计算。例如只选Mean不计算分位数,只选形态不计算HU统计;只选一个组成比值时也不输出其内部依赖的组织面积/体积。
124
+
125
+ ## 输入与选层
126
+
127
+ - 必需:CT与BC mask。接受文件路径或nibabel图像对象,图像需3D且空间匹配,尺寸以mm解释;不重采样。
128
+ - 可选:Spine mask。不提供时3D计算整幅CT,2D在整幅CT全部层中选中间层。
129
+ - 2D有Spine mask时需`spine_label`,例如7或8。仅对该标签实际出现的切片排序,奇数取中间一层,偶数取中间两层。
130
+ - 3D有Spine mask时默认范围`spine_range=(7, 18)`,可配置,取首末层之间的完整范围。新库依据affine识别上下方向,支持数组轴置换。
131
+ - 每个选中切片的组织ROI来自完整BC mask平面,Spine mask仅定位。斜位影像保持原切片平面。
132
+ - 2D两层基础特征先逐层计算再等权平均。独立校正使用这个平均后的形态值;骨校正因此是“平均组织面积/平均骨面积”,不同于旧脚本的“两层骨比值平均”。
133
+ - SD使用ddof=0;分位数为线性插值;非有限CT值不进入HU统计;空组织的面积/体积为0,HU统计为NaN;任一层某基础指标NaN,则两层平均也为NaN。
134
+
135
+ 诊断信息在`extractor.last_diagnostics`,与纯数值特征分开。无Spine mask请传`None`;填写了错误文件路径不会自动退回全图计算。
136
+
137
+ ## CSV分两步运行
138
+
139
+ 新库的CSV使用有意义的列名(区分大小写),不同于旧脚本按列位置读取:
140
+
141
+ ```csv
142
+ ID,Image,Mask,SpineMask,Height_cm,Weight_kg
143
+ 001,ct.nii.gz,bc_mask.nii.gz,,170,65
144
+ ```
145
+
146
+ 必需`Image`、`Mask`,其他列可选;`SpineMask`可省略或留空。相对路径相对于输入CSV目录。`Height_cm`、`Weight_kg`、可选`BSA_m2`由校正步骤读取,逐病例值优先于命令行公共值。
147
+
148
+ ```bash
149
+ # 查询特征名
150
+ ctbodycomp features --mode 3D
151
+
152
+ # 仅提取肌肉、皮下脂肪和骨的体积
153
+ ctbodycomp extract cases.csv raw.csv --mode 3D --config examples/morphometry_3d.json
154
+
155
+ # 对提取结果进行校正,原始列也保留在输出中
156
+ ctbodycomp normalize raw.csv corrected.csv --mode 3D --methods height weight bsa bone --tissues SM SF
157
+
158
+ # 2D单标签、部分特征
159
+ ctbodycomp extract cases.csv raw_2d.csv --mode 2D --label 7 --config examples/selected_2d.json
160
+ ctbodycomp normalize raw_2d.csv corrected_2d.csv --mode 2D --methods bone --tissues SM
161
+ ```
162
+
163
+ 需要校正复合时,提取配置必须包含目标组织的`firstorder_*_Mean`。在同一校正步骤增加`--include-composite`即可:
164
+
165
+ ```bash
166
+ # 示例配置包含肌肉、骨体积和肌肉Mean
167
+ ctbodycomp extract cases.csv raw.csv --mode 3D --config examples/normalized_composite_3d.json
168
+ ctbodycomp normalize raw.csv corrected.csv --mode 3D --methods height weight bsa bone --tissues SM --include-composite
169
+
170
+ # 查询校正复合特征及所需输入
171
+ ctbodycomp features --mode 3D --normalized-composite
172
+ ```
173
+
174
+ 校正CSV保留基础特征,按“校正指数、对应复合项”的顺序追加列,使用统一的`normalization_status`、`normalization_error`记录结果。缺少所需Mean列时记录失败;Mean数值为空时对应复合项为NaN。影像不存在也不影响已成功提取结果的后续计算。
175
+
176
+ 如需对已有校正CSV补算复合项,也保留单独命令:`ctbodycomp normalized-composite corrected.csv products.csv --mode 3D --methods height bone --tissues SM`。
177
+
178
+ 配置JSON直接写“类别→特征名称列表”。命令返回0表示成功,1表示输出中存在失败病例,参数错误返回2。输出目录需存在,输出不能覆盖输入文件;重复输出路径会替换先前结果。
179
+
180
+ ## 完整使用示例
181
+
182
+ 源码包中的 `examples/reference_usage/README.md` 提供五种完整提取示例:2D T12、2D无脊柱、3D T12、3D T1–T12、3D无脊柱,以及独立校正示例。每种情况同时提供可运行的Python脚本和Markdown教程,不依赖额外本地辅助脚本。
183
+
184
+ 输入使用 `cases.example.csv` 和 `cases_without_spine.example.csv` 通用模板。模板不包含实际病例数据,使用前替换为自己的影像路径。示例计算结果写入示例目录下的 `results/`,不会纳入发布包。
185
+
186
+ ## 测试和构建
187
+
188
+ 在项目根目录运行:
189
+
190
+ ```bash
191
+ python -m pip install -e '.[test]'
192
+ python -m pytest -q -p no:cacheprovider
193
+ python -m pip install --upgrade build twine
194
+ python -m build
195
+ python -m twine check dist/ctbodycomp-0.1.1-py3-none-any.whl dist/ctbodycomp-0.1.1.tar.gz
196
+ ```
197
+
198
+ wheel只包含库模块和安装元数据;源码包另外包含测试与使用教程。虚拟环境、缓存、本地影像、实际病例表和计算结果不纳入发布文件。`PUBLISHING.md` 仅保留在开发目录供维护者参考,不随发布包分发。
199
+
200
+ 作者、邮箱和许可证字段当前未设置,可在后续版本补充。本项目目前没有声明MIT或Apache-2.0授权。
@@ -0,0 +1,3 @@
1
+ {
2
+ "morphometry": ["SM_Volume", "SF_Volume", "Bone_Volume"]
3
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "morphometry": ["SM_Volume", "Bone_Volume"],
3
+ "firstorder": ["SM_Mean"]
4
+ }
@@ -0,0 +1,107 @@
1
+ # 2D T12 单标签特征提取
2
+
3
+ 脊柱 mask 的标签 7 对应 T12。候选切片是实际含标签 7 的切片;奇数取中间一层,偶数取中间两层,分别计算各项特征后取算术平均。BC mask 决定各组织的区域。
4
+
5
+ 下面是完整示例,直接复制到自己的 Python 脚本或 Notebook 中运行即可。代码独立可运行,只需要安装 `ctbodycomp`。默认提取全部 **100 项基础特征**,本步骤不进行校正。
6
+
7
+ ## 输入和输出
8
+
9
+ - 输入 CSV:`cases.example.csv`。
10
+ - 必需列:`ID`、`Image`、`Mask`、`SpineMask`。
11
+ - 输出 CSV:`results/01_2d_t12/raw_features.csv`。
12
+ - CT、BC、Spine 的形状和 affine 必须一致。路径可以是绝对路径,也可以相对于输入 CSV 所在目录。
13
+
14
+ ## 完整 Python 代码
15
+
16
+ 对应的 Python 文件已同步为完整代码,可以直接运行:
17
+
18
+ ```bash
19
+ python 01_2d_t12.py
20
+ ```
21
+
22
+ 脚本默认读取同目录的通用 CSV 模板。先填写模板中的实际影像路径,再运行;也可以修改代码中的 input_csv。在 Notebook 中运行时,先将工作目录切换到模板所在目录。
23
+
24
+ ```python
25
+ import json
26
+ from pathlib import Path
27
+
28
+ import pandas as pd
29
+ from ctbodycomp import BodyCompositionExtractor
30
+
31
+ # 1. 输入路径和输出路径:需要换数据时修改这里。
32
+ example_dir = Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd()
33
+ input_csv = example_dir / "cases.example.csv"
34
+ output_csv = input_csv.parent / "results" / "01_2d_t12" / "raw_features.csv"
35
+
36
+ # 2. 读取 CSV 路径表。ID 按字符串读取,可保留开头的 0。
37
+ cases = pd.read_csv(
38
+ input_csv, dtype=str, keep_default_na=False, encoding="utf-8-sig"
39
+ )
40
+ required_columns = {"ID", "Image", "Mask", "SpineMask"}
41
+ if not required_columns.issubset(cases.columns):
42
+ raise ValueError("本例需要 ID、Image、Mask、SpineMask 四列")
43
+ path_columns = ("Image", "Mask", "SpineMask")
44
+ if cases.empty:
45
+ raise ValueError("输入 CSV 没有病例")
46
+
47
+ # 把相对路径转成绝对路径,方便保存后继续处理。
48
+ for column in path_columns:
49
+ cases[column] = cases[column].map(
50
+ lambda value: str((input_csv.parent / Path(value.strip()).expanduser()).resolve())
51
+ if value.strip() else ""
52
+ )
53
+
54
+ # 3. 2D 单标签:T12=7。
55
+ # 默认全部 100 项;可参照 README.md 的特征选择部分修改。
56
+ extractor = BodyCompositionExtractor(mode="2D", spine_label=7)
57
+ feature_names = extractor.get_feature_names()
58
+
59
+ # 4. 逐个病例读取 CT 和 mask,提取基础特征。
60
+ rows = []
61
+ for case in cases.to_dict(orient="records"):
62
+ row = {
63
+ **case,
64
+ **dict.fromkeys(feature_names, float("nan")),
65
+ "diagnostics_scope": "",
66
+ "diagnostics_slices": "",
67
+ "extraction_status": "failed",
68
+ "extraction_error": "",
69
+ }
70
+ try:
71
+ if not case["Image"] or not case["Mask"]:
72
+ raise ValueError("Image 和 Mask 不能为空")
73
+ if not case["SpineMask"]:
74
+ raise ValueError("本例必须提供 SpineMask;无脊柱请使用 without_spine 教程")
75
+ # execute 才真正根据路径读取影像文件。
76
+ features = extractor.execute(
77
+ image=case["Image"],
78
+ mask=case["Mask"],
79
+ spine_mask=case["SpineMask"],
80
+ )
81
+ row.update(features)
82
+ row["diagnostics_scope"] = extractor.last_diagnostics["scope"]
83
+ row["diagnostics_slices"] = json.dumps(
84
+ extractor.last_diagnostics["selected_slices"]
85
+ )
86
+ row["extraction_status"] = "success"
87
+ except Exception as error:
88
+ # 单个病例失败会记入结果,继续处理其余病例。
89
+ row["extraction_error"] = str(error)
90
+ rows.append(row)
91
+
92
+ # 5. 保存结果。原路径列、ID 和可能存在的身高体重列都会保留。
93
+ output_csv.parent.mkdir(parents=True, exist_ok=True)
94
+ results = pd.DataFrame(rows)
95
+ results.to_csv(output_csv, index=False, encoding="utf-8-sig")
96
+ print("基础特征已保存:", output_csv)
97
+ print(results[["ID", "extraction_status", "extraction_error"]].to_string(index=False))
98
+ ```
99
+
100
+ `extraction_status=success` 表示流程完成。某组织没有体素、均值不可计算或比值分母为零时,个别特征仍可能为 0 或 NaN。`diagnostics_slices` 是实际使用的切片索引,从 0 开始。
101
+
102
+ ## 提取后校正
103
+
104
+ 本例的校正模式应为 `2D`。用上面生成的 `raw_features.csv` 作为输入,参考 [提取后独立校正](06_提取后校正.md),不需要重新提取影像特征。
105
+
106
+ [返回教程目录](README.md)
107
+
@@ -0,0 +1,75 @@
1
+ """2D T12 单标签特征提取:直接读取 CSV、执行计算并保存结果。"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ from ctbodycomp import BodyCompositionExtractor
8
+
9
+ # 1. 输入路径和输出路径:需要换数据时修改这里。
10
+ example_dir = Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd()
11
+ input_csv = example_dir / "cases.example.csv"
12
+ output_csv = input_csv.parent / "results" / "01_2d_t12" / "raw_features.csv"
13
+
14
+ # 2. 读取 CSV 路径表。ID 按字符串读取,可保留开头的 0。
15
+ cases = pd.read_csv(
16
+ input_csv, dtype=str, keep_default_na=False, encoding="utf-8-sig"
17
+ )
18
+ required_columns = {"ID", "Image", "Mask", "SpineMask"}
19
+ if not required_columns.issubset(cases.columns):
20
+ raise ValueError("本例需要 ID、Image、Mask、SpineMask 四列")
21
+ path_columns = ("Image", "Mask", "SpineMask")
22
+ if cases.empty:
23
+ raise ValueError("输入 CSV 没有病例")
24
+
25
+ # 把相对路径转成绝对路径,方便保存后继续处理。
26
+ for column in path_columns:
27
+ cases[column] = cases[column].map(
28
+ lambda value: str((input_csv.parent / Path(value.strip()).expanduser()).resolve())
29
+ if value.strip() else ""
30
+ )
31
+
32
+ # 3. 2D 单标签:T12=7。
33
+ # 默认全部 100 项;可参照 README.md 的特征选择部分修改。
34
+ extractor = BodyCompositionExtractor(mode="2D", spine_label=7)
35
+ feature_names = extractor.get_feature_names()
36
+
37
+ # 4. 逐个病例读取 CT 和 mask,提取基础特征。
38
+ rows = []
39
+ for case in cases.to_dict(orient="records"):
40
+ row = {
41
+ **case,
42
+ **dict.fromkeys(feature_names, float("nan")),
43
+ "diagnostics_scope": "",
44
+ "diagnostics_slices": "",
45
+ "extraction_status": "failed",
46
+ "extraction_error": "",
47
+ }
48
+ try:
49
+ if not case["Image"] or not case["Mask"]:
50
+ raise ValueError("Image 和 Mask 不能为空")
51
+ if not case["SpineMask"]:
52
+ raise ValueError("本例必须提供 SpineMask;无脊柱请使用 without_spine 教程")
53
+ # execute 才真正根据路径读取影像文件。
54
+ features = extractor.execute(
55
+ image=case["Image"],
56
+ mask=case["Mask"],
57
+ spine_mask=case["SpineMask"],
58
+ )
59
+ row.update(features)
60
+ row["diagnostics_scope"] = extractor.last_diagnostics["scope"]
61
+ row["diagnostics_slices"] = json.dumps(
62
+ extractor.last_diagnostics["selected_slices"]
63
+ )
64
+ row["extraction_status"] = "success"
65
+ except Exception as error:
66
+ # 单个病例失败会记入结果,继续处理其余病例。
67
+ row["extraction_error"] = str(error)
68
+ rows.append(row)
69
+
70
+ # 5. 保存结果。原路径列、ID 和可能存在的身高体重列都会保留。
71
+ output_csv.parent.mkdir(parents=True, exist_ok=True)
72
+ results = pd.DataFrame(rows)
73
+ results.to_csv(output_csv, index=False, encoding="utf-8-sig")
74
+ print("基础特征已保存:", output_csv)
75
+ print(results[["ID", "extraction_status", "extraction_error"]].to_string(index=False))