imc-histotools 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guojun Han Laboratory @PKU
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: imc-histotools
3
+ Version: 0.1.0
4
+ Summary: Digital pathology analysis tools for the Selenium (Se) IMC paper: virtual IHC rendering, Qupath ROI annotation, and pixel-wise H-score classification.
5
+ Author: Guojun Han Laboratory @PKU
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/gjhan100/IMC-HistoTools
8
+ Project-URL: Repository, https://github.com/gjhan100/IMC-HistoTools
9
+ Project-URL: Issues, https://github.com/gjhan100/IMC-HistoTools/issues
10
+ Keywords: IMC,imaging mass cytometry,digital pathology,virtual IHC,H-score,pixel-wise H-score,Qupath,geojson,selenium
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
16
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.20
21
+ Requires-Dist: opencv-python>=4.5
22
+ Requires-Dist: matplotlib>=3.4
23
+ Requires-Dist: tifffile>=2021.0
24
+ Dynamic: license-file
25
+
26
+ <p align="center"><img src="https://raw.githubusercontent.com/gjhan100/IMC-HistoTools/main/assets/261013_Github.png" width="700"></p>
27
+
28
+ [![Python Versions](https://img.shields.io/badge/python-3.8+-brightgreen.svg)](https://pypi.org/project/imc-histotools)
29
+ [![PyPI](https://img.shields.io/pypi/v/imc-histotools.svg?color=brightgreen&style=flat)](https://pypi.org/project/imc-histotools)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ **IMC-HistoTools** is a digital-pathology toolkit that bridges **imaging mass cytometry (IMC)** and **histopathology**, developed for the Selenium (Se) IMC study. It turns raw IMC channels into brightfield-like virtual IHC images, quantifies marker expression as **pixel-wise H-scores** on Qupath annotations, and renders **virtual multiplex** images from consecutive sections.
33
+
34
+ # Functionality
35
+
36
+ | Function | Description |
37
+ | --- | --- |
38
+ | `virtual_IHC` | Synthesise an IHC / brightfield-like RGB image from IMC channels using the Beer–Lambert absorbance model (per-channel gamma, background threshold, stain colour bar). |
39
+ | `virtual_IHC_ROI` | Same as above but zooms into a local region while **keeping the original pixel coordinates**. |
40
+ | `virtual_IHC_annotation` | Rasterise Qupath `.geojson` polygon annotations into ROI boolean masks. |
41
+ | `Hscore_threshold` | Build classification thresholds from reference ROIs (Negative / Low / Medium / High) using **mean / median / geometric-mean / all**. |
42
+ | `pix_Hscore_map` | Classify every pixel into NP / LP / MP / HP, export a per-pixel CSV, and produce publication figures (classification map + bar chart & table). |
43
+
44
+ # Install IMC-HistoTools
45
+
46
+ ### Using pip [![PyPI](https://img.shields.io/pypi/v/imc-histotools.svg?color=brightgreen&style=flat)](https://pypi.org/project/imc-histotools)
47
+ ```console
48
+ pip install imc-histotools
49
+ ```
50
+
51
+ ### From GitHub [![GitHub](https://img.shields.io/badge/github-source-blue.svg)](https://github.com/gjhan100/IMC-HistoTools)
52
+ ```console
53
+ git clone https://github.com/gjhan100/IMC-HistoTools.git
54
+ cd IMC-HistoTools
55
+ pip install -e .
56
+ ```
57
+
58
+ # Usage
59
+
60
+ <details>
61
+ <summary><strong>1. Virtual IHC from IMC raw tiff</strong></summary>
62
+
63
+ Render a brightfield-like IHC image from raw (or [0,1]-normalised) IMC channels.
64
+ `Blue` is typically the nuclear marker (DNA); `Brown`/`Red` are membrane / lineage markers.
65
+
66
+ ```python
67
+ import numpy as np
68
+ import tifffile
69
+ from IMC_HistoTools import virtual_IHC, virtual_IHC_ROI
70
+
71
+ # read IMC channels
72
+ dna = tifffile.imread("ROI_001_193Ir-DNA_cor.tiff").astype(np.float32) # nuclear
73
+ se = tifffile.imread("ROI_001_Se-Ecadherin_cor.tiff").astype(np.float32) # membrane
74
+
75
+ # full-image virtual IHC (Blue = DNA, Brown = E-cadherin-Se)
76
+ virtual_IHC(
77
+ Blue_image=dna, Red_image=None, Brown_image=se,
78
+ blue_name="DNA", brown_name="E-cadherin (Se)",
79
+ brown_threshold=0.1, # background removal (< threshold -> 0)
80
+ brown_bar=True, # show stain colour bar
81
+ save_dir="out/virtual_IHC.png",
82
+ )
83
+
84
+ # local region, keeping original pixel coordinates
85
+ virtual_IHC_ROI(
86
+ Blue_image=dna, Red_image=None, Brown_image=se,
87
+ X0=2250, X1=3000, Y0=1250, Y1=2000, TICK=250,
88
+ save_dir="out/virtual_IHC_ROI.png",
89
+ )
90
+ ```
91
+ </details>
92
+
93
+ <details>
94
+ <summary><strong>2. Pixel-wise H-score with Qupath annotation</strong></summary>
95
+
96
+ Load tissue-region and reference-ROI annotations exported from Qupath, calibrate the
97
+ thresholds from the Negative / Low / Medium / High reference ROIs, then classify every
98
+ pixel and export a CSV + figures.
99
+
100
+ ```python
101
+ from IMC_HistoTools import virtual_IHC_annotation, Hscore_threshold, pix_Hscore_map
102
+
103
+ marker = tifffile.imread("Normalize/ROI_001_Se_crop_normal.tiff").astype(np.float32)
104
+ tissue = tifffile.imread("Mask/mask_crop.tiff") > 0
105
+
106
+ # 1) tissue-region annotations (Normal mucosa / Tumor center / Invasive front / ...)
107
+ ann = virtual_IHC_annotation(marker, "tissue_regions.geojson")
108
+
109
+ # 2) reference ROIs (Negative / Low / Medium / High) for threshold calibration
110
+ ref = virtual_IHC_annotation(marker, "reference_ROIs.geojson",
111
+ region_order=["Negative", "Low", "Medium", "High"])
112
+
113
+ # 3) thresholds from the reference ROIs (stat: 'mean'/'median'/'geomean'/'all')
114
+ thr = Hscore_threshold(
115
+ marker,
116
+ Np=ref["masks"]["Negative"], Lp=ref["masks"]["Low"],
117
+ Mp=ref["masks"]["Medium"], Hp=ref["masks"]["High"],
118
+ tissue_mask=tissue, stat="median",
119
+ )
120
+ print(thr["thresholds"]) # (Lp, Mp, Hp)
121
+
122
+ # 4) per-pixel H-score map + CSV + bar chart / table
123
+ res = pix_Hscore_map(
124
+ marker,
125
+ Hscore_threshold_annotation=ref["masks"], # Lp / Mp / Hp reference ROIs
126
+ tissue_annotation=ann["masks"], # non-empty -> bar chart + table
127
+ tissue_mask=tissue,
128
+ stat="median",
129
+ delta=2, kernel=(5, 5), # Gaussian blur for the map display only
130
+ save_dir="out/",
131
+ )
132
+ res["csv_path"] # per-pixel classification: x, y, intensity, score, category
133
+ ```
134
+
135
+ Classification: `intensity < Lp` → 0 (NP), `[Lp, Mp)` → 1 (LP), `[Mp, Hp)` → 2 (MP),
136
+ `>= Hp` → 3 (HP). The bar-chart statistics always use the **non-blurred** classification.
137
+ </details>
138
+
139
+ <details>
140
+ <summary><strong>3. Virtual multiplexing from consecutive sections</strong></summary>
141
+
142
+ Feed registered channels from consecutive sections into the three colour channels of
143
+ `virtual_IHC` to obtain a single virtual-multiplex image (blue / red / brown).
144
+
145
+ ```python
146
+ from IMC_HistoTools import virtual_IHC
147
+
148
+ virtual_IHC(
149
+ Blue_image=dna, # section A — DNA (nuclear, blue)
150
+ Red_image=cd45, # section B — CD45 (red)
151
+ Brown_image=ecad, # section C — E-cadherin (membrane, brown)
152
+ blue_name="DNA", red_name="CD45", brown_name="E-cadherin",
153
+ blue_bar=True, red_bar=True, brown_bar=True,
154
+ save_dir="out/virtual_multiplex.png",
155
+ )
156
+ ```
157
+ </details>
158
+
159
+ # Citation
160
+
161
+ If you use **IMC-HistoTools** in your research, please cite the Se IMC paper (citation
162
+ to be added upon publication).
163
+
164
+ # License
165
+
166
+ This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file.
@@ -0,0 +1,141 @@
1
+ <p align="center"><img src="https://raw.githubusercontent.com/gjhan100/IMC-HistoTools/main/assets/261013_Github.png" width="700"></p>
2
+
3
+ [![Python Versions](https://img.shields.io/badge/python-3.8+-brightgreen.svg)](https://pypi.org/project/imc-histotools)
4
+ [![PyPI](https://img.shields.io/pypi/v/imc-histotools.svg?color=brightgreen&style=flat)](https://pypi.org/project/imc-histotools)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **IMC-HistoTools** is a digital-pathology toolkit that bridges **imaging mass cytometry (IMC)** and **histopathology**, developed for the Selenium (Se) IMC study. It turns raw IMC channels into brightfield-like virtual IHC images, quantifies marker expression as **pixel-wise H-scores** on Qupath annotations, and renders **virtual multiplex** images from consecutive sections.
8
+
9
+ # Functionality
10
+
11
+ | Function | Description |
12
+ | --- | --- |
13
+ | `virtual_IHC` | Synthesise an IHC / brightfield-like RGB image from IMC channels using the Beer–Lambert absorbance model (per-channel gamma, background threshold, stain colour bar). |
14
+ | `virtual_IHC_ROI` | Same as above but zooms into a local region while **keeping the original pixel coordinates**. |
15
+ | `virtual_IHC_annotation` | Rasterise Qupath `.geojson` polygon annotations into ROI boolean masks. |
16
+ | `Hscore_threshold` | Build classification thresholds from reference ROIs (Negative / Low / Medium / High) using **mean / median / geometric-mean / all**. |
17
+ | `pix_Hscore_map` | Classify every pixel into NP / LP / MP / HP, export a per-pixel CSV, and produce publication figures (classification map + bar chart & table). |
18
+
19
+ # Install IMC-HistoTools
20
+
21
+ ### Using pip [![PyPI](https://img.shields.io/pypi/v/imc-histotools.svg?color=brightgreen&style=flat)](https://pypi.org/project/imc-histotools)
22
+ ```console
23
+ pip install imc-histotools
24
+ ```
25
+
26
+ ### From GitHub [![GitHub](https://img.shields.io/badge/github-source-blue.svg)](https://github.com/gjhan100/IMC-HistoTools)
27
+ ```console
28
+ git clone https://github.com/gjhan100/IMC-HistoTools.git
29
+ cd IMC-HistoTools
30
+ pip install -e .
31
+ ```
32
+
33
+ # Usage
34
+
35
+ <details>
36
+ <summary><strong>1. Virtual IHC from IMC raw tiff</strong></summary>
37
+
38
+ Render a brightfield-like IHC image from raw (or [0,1]-normalised) IMC channels.
39
+ `Blue` is typically the nuclear marker (DNA); `Brown`/`Red` are membrane / lineage markers.
40
+
41
+ ```python
42
+ import numpy as np
43
+ import tifffile
44
+ from IMC_HistoTools import virtual_IHC, virtual_IHC_ROI
45
+
46
+ # read IMC channels
47
+ dna = tifffile.imread("ROI_001_193Ir-DNA_cor.tiff").astype(np.float32) # nuclear
48
+ se = tifffile.imread("ROI_001_Se-Ecadherin_cor.tiff").astype(np.float32) # membrane
49
+
50
+ # full-image virtual IHC (Blue = DNA, Brown = E-cadherin-Se)
51
+ virtual_IHC(
52
+ Blue_image=dna, Red_image=None, Brown_image=se,
53
+ blue_name="DNA", brown_name="E-cadherin (Se)",
54
+ brown_threshold=0.1, # background removal (< threshold -> 0)
55
+ brown_bar=True, # show stain colour bar
56
+ save_dir="out/virtual_IHC.png",
57
+ )
58
+
59
+ # local region, keeping original pixel coordinates
60
+ virtual_IHC_ROI(
61
+ Blue_image=dna, Red_image=None, Brown_image=se,
62
+ X0=2250, X1=3000, Y0=1250, Y1=2000, TICK=250,
63
+ save_dir="out/virtual_IHC_ROI.png",
64
+ )
65
+ ```
66
+ </details>
67
+
68
+ <details>
69
+ <summary><strong>2. Pixel-wise H-score with Qupath annotation</strong></summary>
70
+
71
+ Load tissue-region and reference-ROI annotations exported from Qupath, calibrate the
72
+ thresholds from the Negative / Low / Medium / High reference ROIs, then classify every
73
+ pixel and export a CSV + figures.
74
+
75
+ ```python
76
+ from IMC_HistoTools import virtual_IHC_annotation, Hscore_threshold, pix_Hscore_map
77
+
78
+ marker = tifffile.imread("Normalize/ROI_001_Se_crop_normal.tiff").astype(np.float32)
79
+ tissue = tifffile.imread("Mask/mask_crop.tiff") > 0
80
+
81
+ # 1) tissue-region annotations (Normal mucosa / Tumor center / Invasive front / ...)
82
+ ann = virtual_IHC_annotation(marker, "tissue_regions.geojson")
83
+
84
+ # 2) reference ROIs (Negative / Low / Medium / High) for threshold calibration
85
+ ref = virtual_IHC_annotation(marker, "reference_ROIs.geojson",
86
+ region_order=["Negative", "Low", "Medium", "High"])
87
+
88
+ # 3) thresholds from the reference ROIs (stat: 'mean'/'median'/'geomean'/'all')
89
+ thr = Hscore_threshold(
90
+ marker,
91
+ Np=ref["masks"]["Negative"], Lp=ref["masks"]["Low"],
92
+ Mp=ref["masks"]["Medium"], Hp=ref["masks"]["High"],
93
+ tissue_mask=tissue, stat="median",
94
+ )
95
+ print(thr["thresholds"]) # (Lp, Mp, Hp)
96
+
97
+ # 4) per-pixel H-score map + CSV + bar chart / table
98
+ res = pix_Hscore_map(
99
+ marker,
100
+ Hscore_threshold_annotation=ref["masks"], # Lp / Mp / Hp reference ROIs
101
+ tissue_annotation=ann["masks"], # non-empty -> bar chart + table
102
+ tissue_mask=tissue,
103
+ stat="median",
104
+ delta=2, kernel=(5, 5), # Gaussian blur for the map display only
105
+ save_dir="out/",
106
+ )
107
+ res["csv_path"] # per-pixel classification: x, y, intensity, score, category
108
+ ```
109
+
110
+ Classification: `intensity < Lp` → 0 (NP), `[Lp, Mp)` → 1 (LP), `[Mp, Hp)` → 2 (MP),
111
+ `>= Hp` → 3 (HP). The bar-chart statistics always use the **non-blurred** classification.
112
+ </details>
113
+
114
+ <details>
115
+ <summary><strong>3. Virtual multiplexing from consecutive sections</strong></summary>
116
+
117
+ Feed registered channels from consecutive sections into the three colour channels of
118
+ `virtual_IHC` to obtain a single virtual-multiplex image (blue / red / brown).
119
+
120
+ ```python
121
+ from IMC_HistoTools import virtual_IHC
122
+
123
+ virtual_IHC(
124
+ Blue_image=dna, # section A — DNA (nuclear, blue)
125
+ Red_image=cd45, # section B — CD45 (red)
126
+ Brown_image=ecad, # section C — E-cadherin (membrane, brown)
127
+ blue_name="DNA", red_name="CD45", brown_name="E-cadherin",
128
+ blue_bar=True, red_bar=True, brown_bar=True,
129
+ save_dir="out/virtual_multiplex.png",
130
+ )
131
+ ```
132
+ </details>
133
+
134
+ # Citation
135
+
136
+ If you use **IMC-HistoTools** in your research, please cite the Se IMC paper (citation
137
+ to be added upon publication).
138
+
139
+ # License
140
+
141
+ This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file.
@@ -0,0 +1,24 @@
1
+ """
2
+ IMC-HistoTools — digital pathology analysis toolkit for the Selenium (Se) IMC paper.
3
+
4
+ Provides:
5
+ * virtual_IHC : Beer-Lambert virtual IHC (brightfield-like) rendering
6
+ * virtual_IHC_ROI : local-region rendering that keeps original pixel coords
7
+ * virtual_IHC_annotation : Qupath geojson -> ROI masks
8
+ * Hscore_threshold : median-intensity thresholds from Np/Lp/Mp/Hp reference ROIs
9
+ * pix_Hscore_map : pixel-wise H-score classification + CSV + figures
10
+ """
11
+
12
+ from .virtual_IHC import virtual_IHC, virtual_IHC_ROI
13
+ from .annotation_region import virtual_IHC_annotation, Hscore_threshold
14
+ from .pix_Hscore import pix_Hscore_map
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ "virtual_IHC",
20
+ "virtual_IHC_ROI",
21
+ "virtual_IHC_annotation",
22
+ "Hscore_threshold",
23
+ "pix_Hscore_map",
24
+ ]
@@ -0,0 +1,183 @@
1
+ """
2
+ annotation_region.py — Qupath 圈画 ROI 与 H-score 阈值模块 (IMC-HistoTools)
3
+
4
+ 提供:
5
+ * ``virtual_IHC_annotation`` : 读取 Qupath geojson 圈画文件 → 栅格化为 ROI mask
6
+ * ``Hscore_threshold`` : 根据参考 ROI (Np / Lp / Mp / Hp) 计算各层
7
+ 像素中位值, 作为 pixel-wise H-score 分层阈值
8
+
9
+ 对应 2610/261003_Pix_Hscore_histopathology.ipynb 中的 geojson → mask 逻辑,
10
+ 并扩展为可直接复用的包函数。
11
+ """
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ import cv2
18
+
19
+
20
+ # ============================================================
21
+ # helper
22
+ # ============================================================
23
+
24
+ def _as_mask(roi, shape):
25
+ """把「单个 mask 或 mask 列表」归一化为一个 bool ndarray (列表取并集)."""
26
+ if roi is None:
27
+ return None
28
+ if isinstance(roi, (list, tuple)):
29
+ m = np.zeros(shape, dtype=bool)
30
+ for r in roi:
31
+ if r is None:
32
+ continue
33
+ m |= np.asarray(r, dtype=bool)
34
+ return m
35
+ return np.asarray(roi, dtype=bool)
36
+
37
+
38
+ # ============================================================
39
+ # 1. Qupath geojson → ROI mask
40
+ # ============================================================
41
+
42
+ def virtual_IHC_annotation(image, geojson_path, region_order=None, name_key='name'):
43
+ """
44
+ 读取 Qupath geojson 圈画文件, 栅格化为 ROI 区域的布尔 mask。
45
+
46
+ 仅取每个 polygon 的外环 (第一个坐标环), 用 ``cv2.fillPoly`` 填充;
47
+ 支持 Polygon 与 MultiPolygon。
48
+
49
+ Parameters
50
+ ----------
51
+ image : 2D ndarray 或 (H, W) tuple
52
+ 用于确定栅格尺寸 H, W (通常传入对应的 virtual IHC / marker 图像)。
53
+ geojson_path : str or Path
54
+ Qupath 导出的 .geojson 文件路径。
55
+ region_order : list of str or None
56
+ 返回结果的 region 顺序; None → 按 geojson 中出现顺序去重。
57
+ name_key : str
58
+ feature['properties'] 中用于区分区域名称的键 (默认 'name')。
59
+
60
+ Returns
61
+ -------
62
+ dict
63
+ {
64
+ 'polys' : {name: [xy ndarray (N,2), ...]}, # 每个区域的外环顶点 (x, y)
65
+ 'masks' : {name: bool ndarray (H, W)}, # 每个区域的栅格 mask
66
+ 'names' : [按顺序的 region 名],
67
+ }
68
+ """
69
+ image = np.asarray(image)
70
+ if image.ndim == 2:
71
+ H, W = image.shape
72
+ else:
73
+ H, W = int(image[0]), int(image[1])
74
+
75
+ with open(geojson_path) as f:
76
+ gj = json.load(f)
77
+
78
+ region_polys = {} # name -> [exterior xy array, ...]
79
+ for feat in gj['features']:
80
+ name = feat['properties'].get(name_key, 'unknown')
81
+ geom = feat['geometry']
82
+ if geom['type'] == 'MultiPolygon':
83
+ polys = geom['coordinates']
84
+ else:
85
+ polys = [geom['coordinates']]
86
+ for poly in polys:
87
+ # 外环 (x, y)
88
+ region_polys.setdefault(name, []).append(
89
+ np.asarray(poly[0], dtype=np.float64))
90
+
91
+ if region_order is None:
92
+ region_order = list(region_polys.keys())
93
+
94
+ region_masks = {}
95
+ for name in region_order:
96
+ m = np.zeros((H, W), np.uint8)
97
+ for xy in region_polys.get(name, []):
98
+ pts = np.round(xy).astype(np.int32).reshape(-1, 1, 2) # cv2: (x, y)
99
+ cv2.fillPoly(m, [pts], 1)
100
+ region_masks[name] = m.astype(bool)
101
+
102
+ return {
103
+ 'polys': {n: region_polys.get(n, []) for n in region_order},
104
+ 'masks': region_masks,
105
+ 'names': region_order,
106
+ }
107
+
108
+
109
+ # ============================================================
110
+ # 2. 参考 ROI → H-score 分层阈值 (中位值)
111
+ # ============================================================
112
+
113
+ def Hscore_threshold(marker_image, Np=None, Lp=None, Mp=None, Hp=None,
114
+ tissue_mask=None, exclude_zero=False, stat='median'):
115
+ """
116
+ 根据参考 ROI (Np / Lp / Mp / Hp) 统计像素强度, 作为返回的阈值。
117
+
118
+ 对每个参考 ROI 分别统计:
119
+ - mean : 平均值
120
+ - median : 中位值
121
+ - geomean : 几何平均值 (仅正像素)
122
+ - all : mean / median / geomean 三者的平均
123
+
124
+ Np / Lp / Mp / Hp 分别对应单个或多个设置 ROI 区域 (bool mask 或 mask 列表,
125
+ 列表取并集); 可传 ``tissue_mask`` 以仅统计落在组织内的像素。
126
+
127
+ Parameters
128
+ ----------
129
+ marker_image : 2D ndarray
130
+ marker 强度图像 (通常为背景扣除后的图像)。
131
+ Np, Lp, Mp, Hp : bool ndarray 或 list of bool ndarray, 或 None
132
+ 负 / 低 / 中 / 高 表达的参考 ROI 区域。
133
+ tissue_mask : bool ndarray or None
134
+ 组织 mask (True=组织); 提供时各 ROI 会与组织求交。
135
+ exclude_zero : bool
136
+ 是否在统计前排除强度为 0 的像素 (默认 False)。
137
+ stat : str, {'mean','median','geomean','all'}
138
+ 选择用于 ``thresholds`` 的统计量 (默认 'median')。
139
+
140
+ Returns
141
+ -------
142
+ dict
143
+ {
144
+ 'Np' : {'mean','median','geomean','all'} 或 None,
145
+ 'Lp' : {...}, 'Mp' : {...}, 'Hp' : {...},
146
+ 'thresholds' : (T1, T2, T3), # 由 stat 决定, 对应 Lp/Mp/Hp
147
+ 'stat' : str,
148
+ }
149
+ """
150
+ if stat not in ('mean', 'median', 'geomean', 'all'):
151
+ raise ValueError(f"stat 必须是 'mean'/'median'/'geomean'/'all', 得到 {stat!r}")
152
+
153
+ img = np.asarray(marker_image, dtype=np.float32)
154
+ shape = img.shape
155
+
156
+ def _stats(roi):
157
+ m = _as_mask(roi, shape)
158
+ if m is None:
159
+ return None
160
+ if tissue_mask is not None:
161
+ m = m & np.asarray(tissue_mask, dtype=bool)
162
+ vals = img[m].astype(np.float64)
163
+ if exclude_zero:
164
+ vals = vals[vals > 0]
165
+ if vals.size == 0:
166
+ return None
167
+ mean = float(np.mean(vals))
168
+ median = float(np.median(vals))
169
+ pos = vals[vals > 0]
170
+ geomean = float(np.exp(np.mean(np.log(pos)))) if pos.size else 0.0
171
+ all_ = (mean + median + geomean) / 3.0
172
+ return {'mean': mean, 'median': median, 'geomean': geomean, 'all': all_}
173
+
174
+ stats = {'Np': _stats(Np), 'Lp': _stats(Lp),
175
+ 'Mp': _stats(Mp), 'Hp': _stats(Hp)}
176
+
177
+ thresholds = tuple(stats[k][stat] if stats[k] is not None else None
178
+ for k in ('Lp', 'Mp', 'Hp'))
179
+
180
+ result = dict(stats)
181
+ result['thresholds'] = thresholds
182
+ result['stat'] = stat
183
+ return result