Skip to content

Commit 77b15c4

Browse files
committed
Merge pull request #94 from DigitalSlideArchive/nuclei_segmentation_cli
ENH: Add CLI to segment nuclei and generate annotations of nuclei bounding boxes
2 parents 4c501c8 + 40e9feb commit 77b15c4

File tree

10 files changed

+316
-24
lines changed

10 files changed

+316
-24
lines changed

.travis.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ cache:
66
directories:
77
- $HOME/.cache
88

9-
sudo: false
9+
sudo: required
10+
11+
services:
12+
- docker
1013

1114
compiler:
1215
- gcc

Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ RUN mkdir -p $build_path && \
2121
chmod +x $build_path/miniconda/bin/python
2222
ENV PATH=$build_path/miniconda/bin:${PATH}
2323

24+
# git clone install ctk-cli
25+
RUN git clone https://github.com/cdeepakroy/ctk-cli.git && cd ctk-cli \
26+
git checkout 979d8cb671060e787b725b0226332a72a551592e && \
27+
python setup.py install
28+
2429
# copy HistomicsTK files
2530
ENV htk_path=$PWD/HistomicsTK
2631
RUN mkdir -p $htk_path
@@ -29,7 +34,7 @@ WORKDIR $htk_path
2934

3035
# Install HistomicsTK and its dependencies
3136
RUN conda config --add channels https://conda.binstar.org/cdeepakroy && \
32-
conda install --yes libgfortran==1.0 openslide-python \
37+
conda install --yes pip libgfortran==1.0 openslide-python \
3338
--file requirements.txt --file requirements_c_conda.txt && \
3439
pip install -r requirements_c.txt && \
3540
# Install HistomicsTK

histomicstk/MaxClustering.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def MaxClustering(Response, Mask, r=10):
1919
Mask : array_like
2020
A binary image where nuclei pixels have value 1/True, and non-nuclear
2121
pixels have value 0/False.
22-
f : float
22+
r : float
2323
A scalar defining the clustering radius. Default value = 10.
2424
2525
Returns
@@ -61,6 +61,7 @@ def MaxClustering(Response, Mask, r=10):
6161
# pad input array to simplify filtering
6262
I = Response.min() * np.ones((Response.shape[0]+2*r,
6363
Response.shape[1]+2*r))
64+
Response = Response.copy()
6465
Response[~Mask] = Response.min()
6566
I[r:r+Response.shape[0], r:r+Response.shape[1]] = Response
6667

@@ -71,8 +72,8 @@ def MaxClustering(Response, Mask, r=10):
7172

7273
# define pixels for local neighborhoods
7374
py, px = np.nonzero(Mask)
74-
py = py + r
75-
px = px + r
75+
py = py + np.int(r)
76+
px = px + np.int(r)
7677

7778
# perform max filtering
7879
for i in np.arange(0, px.size, 1):

histomicstk/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
from .BinaryMixtureCut import BinaryMixtureCut
3+
# from .BinaryMixtureCut import BinaryMixtureCut
44
from .ChanVese import ChanVese
55
from .cLoG import cLoG
66
from .ColorConvolution import ColorConvolution
@@ -42,8 +42,8 @@
4242

4343
__version__ = '0.1.0'
4444

45-
__all__ = ('BinaryMixtureCut',
46-
'ChanVese',
45+
# Add 'BinaryMixtureCut' after pygco is deployed
46+
__all__ = ('ChanVese',
4747
'cLoG',
4848
'ColorConvolution',
4949
'ColorDeconvolution',

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44
#
55
###############################################################
66
lxml>=3.4.4
7-
ctk-cli>=1.3
7+
8+
9+

requirements_c_conda.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
#
66
#########################################################
77

8+
# temporarily moving this here until 1.3.1 becomes available on pypi
9+
ctk-cli==1.3.1
10+
811
# large_image requires numpy==1.10.2
912
numpy==1.10.2
1013

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from ctk_cli import CLIArgumentParser
2+
import histomicstk as htk
3+
import numpy as np
4+
import json
5+
import scipy as sp
6+
import skimage.io
7+
import skimage.measure
8+
9+
import logging
10+
logging.basicConfig()
11+
12+
stainColorMap = {
13+
'hematoxylin': [0.65, 0.70, 0.29],
14+
'eosin': [0.07, 0.99, 0.11],
15+
'dab': [0.27, 0.57, 0.78],
16+
'null': [0.0, 0.0, 0.0]
17+
}
18+
19+
20+
def main(args):
21+
22+
#
23+
# Read Input Image
24+
#
25+
print('>> Reading input image')
26+
27+
imInput = skimage.io.imread(args.inputImageFile)[:, :, :3]
28+
29+
#
30+
# Perform color normalization
31+
#
32+
print('>> Performing color normalization')
33+
34+
# transform input image to LAB color space
35+
imInputLAB = htk.RudermanLABFwd(imInput)
36+
37+
# compute mean and stddev of input in LAB color space
38+
Mu = np.zeros(3)
39+
Sigma = np.zeros(3)
40+
41+
for i in range(3):
42+
Mu[i] = imInputLAB[:, :, i].mean()
43+
Sigma[i] = (imInputLAB[:, :, i] - Mu[i]).std()
44+
45+
# perform reinhard normalization
46+
imNmzd = htk.ReinhardNorm(imInput, Mu, Sigma)
47+
48+
#
49+
# Perform color deconvolution
50+
#
51+
print('>> Performing color deconvolution')
52+
53+
stainColor_1 = stainColorMap[args.stain_1]
54+
stainColor_2 = stainColorMap[args.stain_2]
55+
stainColor_3 = stainColorMap[args.stain_3]
56+
57+
W = np.array([stainColor_1, stainColor_2, stainColor_3]).T
58+
59+
imDeconvolved = htk.ColorDeconvolution(imNmzd, W)
60+
61+
imNucleiStain = imDeconvolved.Stains[:, :, 0].astype(np.float)
62+
63+
#
64+
# Perform nuclei segmentation
65+
#
66+
print('>> Performing nuclei segmentation')
67+
68+
# segment foreground
69+
imFgndMask = sp.ndimage.morphology.binary_fill_holes(
70+
imNucleiStain < args.foreground_threshold)
71+
72+
# run adaptive multi-scale LoG filter
73+
imLog = htk.cLoG(imNucleiStain, imFgndMask,
74+
SigmaMin=args.min_radius * np.sqrt(2),
75+
SigmaMax=args.max_radius * np.sqrt(2))
76+
77+
imNucleiSegMask, Seeds, Max = htk.MaxClustering(
78+
imLog, imFgndMask, args.local_max_search_radius)
79+
80+
# filter out small objects
81+
imNucleiSegMask = htk.FilterLabel(
82+
imNucleiSegMask, Lower=args.min_nucleus_area).astype(np.int)
83+
84+
#
85+
# Generate annotations
86+
#
87+
objProps = skimage.measure.regionprops(imNucleiSegMask)
88+
89+
print 'Number of nuclei = ', len(objProps)
90+
91+
# create basic schema
92+
annotation = {
93+
"name": "Nuclei",
94+
"description": "Nuclei bounding boxes from a segmentation algorithm",
95+
"attributes": {
96+
"algorithm": {
97+
"color_normalization": "ReinhardNorm",
98+
"color_deconvolution": "ColorDeconvolution",
99+
"nuclei_segmentation": ["cLOG",
100+
"MaxClustering",
101+
"FilterLabel"]
102+
}
103+
},
104+
"elements": []
105+
}
106+
107+
# add each nucleus as an element into the annotation schema
108+
for i in range(len(objProps)):
109+
110+
c = [objProps[i].centroid[1], objProps[i].centroid[0], 0]
111+
width = objProps[i].bbox[3] - objProps[i].bbox[1] + 1
112+
height = objProps[i].bbox[2] - objProps[i].bbox[0] + 1
113+
114+
cur_bbox = {
115+
"type": "rectangle",
116+
"center": c,
117+
"width": width,
118+
"height": height,
119+
}
120+
121+
annotation["elements"].append(cur_bbox)
122+
123+
#
124+
# Save output segmentation mask
125+
#
126+
print('>> Outputting nuclei segmentation mask')
127+
128+
skimage.io.imsave(args.outputNucleiMaskFile, imNucleiSegMask)
129+
130+
#
131+
# Save output annotation
132+
#
133+
print('>> Outputting nuclei annotation')
134+
135+
with open(args.outputNucleiAnnotationFile, 'w') as annotationFile:
136+
json.dump(annotation, annotationFile, indent=2, sort_keys=False)
137+
138+
139+
if __name__ == "__main__":
140+
main(CLIArgumentParser().parse_args())
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<executable>
3+
<category>HistomicsTK</category>
4+
<title>Segments Nuclei</title>
5+
<description>Segments nuclei in a whole-slide image</description>
6+
<version>0.1.0</version>
7+
<documentation-url>https://histomicstk.readthedocs.org/en/latest/</documentation-url>
8+
<license>Apache 2.0</license>
9+
<contributor>Deepak Roy Chittajallu (Kitware)</contributor>
10+
<acknowledgements>This work is part of the HistomicsTK project.</acknowledgements>
11+
<parameters>
12+
<label>IO</label>
13+
<description>Input/output parameters</description>
14+
<image>
15+
<name>inputImageFile</name>
16+
<label>Input Image</label>
17+
<description>Input image to be deconvolved</description>
18+
<channel>input</channel>
19+
<index>0</index>
20+
</image>
21+
<image>
22+
<name>outputNucleiMaskFile</name>
23+
<label>Output Nuclei Segmentation Mask</label>
24+
<description>Output nuclei segmentation label mask</description>
25+
<channel>output</channel>
26+
<index>1</index>
27+
</image>
28+
<file fileExtensions=".anot" reference="inputImageFile">
29+
<name>outputNucleiAnnotationFile</name>
30+
<label>Output Nuclei Annotation File</label>
31+
<description>Output nuclei annotation file</description>
32+
<channel>output</channel>
33+
<index>2</index>
34+
</file>
35+
</parameters>
36+
<parameters>
37+
<label>Color Deconvolution</label>
38+
<description>Color Deconvolution parameters</description>
39+
<string-enumeration>
40+
<name>stain_1</name>
41+
<label>stain-1</label>
42+
<description>Name of stain-1</description>
43+
<channel>input</channel>
44+
<longflag>stain_1</longflag>
45+
<element>hematoxylin</element>
46+
<element>eosin</element>
47+
<element>dab</element>
48+
<default>hematoxylin</default>
49+
</string-enumeration>
50+
<string-enumeration>
51+
<name>stain_2</name>
52+
<label>stain-2</label>
53+
<description>Name of stain-2</description>
54+
<channel>input</channel>
55+
<longflag>stain_2</longflag>
56+
<element>hematoxylin</element>
57+
<element>eosin</element>
58+
<element>dab</element>
59+
<default>eosin</default>
60+
</string-enumeration>
61+
<string-enumeration>
62+
<name>stain_3</name>
63+
<label>stain-3</label>
64+
<description>Name of stain-3</description>
65+
<channel>input</channel>
66+
<longflag>stain_3</longflag>
67+
<element>hematoxylin</element>
68+
<element>eosin</element>
69+
<element>dab</element>
70+
<element>null</element>
71+
<default>null</default>
72+
</string-enumeration>
73+
</parameters>
74+
<parameters>
75+
<label>Nuclei segmentation</label>
76+
<description>Nuclei segmentation parameters</description>
77+
<double>
78+
<name>foreground_threshold</name>
79+
<label>Foreground Intensity Threshold</label>
80+
<description>Intensity value to use as threshold to segment foreground in nuclear stain image</description>
81+
<longflag>foreground_threshold</longflag>
82+
<default>160</default>
83+
</double>
84+
<double>
85+
<name>min_radius</name>
86+
<label>Minimum Radius</label>
87+
<description>Minimum nuclear radius (used to set min sigma of the multiscale LoG filter)</description>
88+
<longflag>min_radius</longflag>
89+
<default>4</default>
90+
</double>
91+
<double>
92+
<name>max_radius</name>
93+
<label>Maximum Radius</label>
94+
<description>Maximum nuclear radius (used to set max sigma of the multiscale LoG filter)</description>
95+
<longflag>max_radius</longflag>
96+
<default>7</default>
97+
</double>
98+
<double>
99+
<name>local_max_search_radius</name>
100+
<label>Local Max Search Radius</label>
101+
<description>Local max search radius used for detection seed points in nuclei</description>
102+
<longflag>local_max_search_radius</longflag>
103+
<default>10</default>
104+
</double>
105+
<double>
106+
<name>min_nucleus_area</name>
107+
<label>Minimum Nucleus Area</label>
108+
<description>Minimum area that each nucleus should have</description>
109+
<longflag>min_nucleus_area</longflag>
110+
<default>80</default>
111+
</double>
112+
</parameters>
113+
</executable>

0 commit comments

Comments
 (0)