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.
- imc_histotools-0.1.0/LICENSE +21 -0
- imc_histotools-0.1.0/PKG-INFO +166 -0
- imc_histotools-0.1.0/README.md +141 -0
- imc_histotools-0.1.0/__init__.py +24 -0
- imc_histotools-0.1.0/annotation_region.py +183 -0
- imc_histotools-0.1.0/imc_histotools.egg-info/PKG-INFO +166 -0
- imc_histotools-0.1.0/imc_histotools.egg-info/SOURCES.txt +16 -0
- imc_histotools-0.1.0/imc_histotools.egg-info/dependency_links.txt +1 -0
- imc_histotools-0.1.0/imc_histotools.egg-info/requires.txt +4 -0
- imc_histotools-0.1.0/imc_histotools.egg-info/top_level.txt +1 -0
- imc_histotools-0.1.0/pix_Hscore.py +427 -0
- imc_histotools-0.1.0/pyproject.toml +47 -0
- imc_histotools-0.1.0/setup.cfg +4 -0
- imc_histotools-0.1.0/virtual_IHC.py +400 -0
|
@@ -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
|
+
[](https://pypi.org/project/imc-histotools)
|
|
29
|
+
[](https://pypi.org/project/imc-histotools)
|
|
30
|
+
[](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 [](https://pypi.org/project/imc-histotools)
|
|
47
|
+
```console
|
|
48
|
+
pip install imc-histotools
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### From GitHub [](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
|
+
[](https://pypi.org/project/imc-histotools)
|
|
4
|
+
[](https://pypi.org/project/imc-histotools)
|
|
5
|
+
[](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 [](https://pypi.org/project/imc-histotools)
|
|
22
|
+
```console
|
|
23
|
+
pip install imc-histotools
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### From GitHub [](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
|