├── .gitignore
├── README.md
├── deephistopath
└── wsi
│ ├── filter.py
│ ├── slide.py
│ ├── tiles.py
│ └── util.py
└── docs
└── wsi-preprocessing-in-python
├── _layouts
└── default.html
├── images
├── 127-rgb-after-filters.png
├── 127-rgb.png
├── 21-avoid-overmask-green-ch-2.png
├── 21-avoid-overmask-green-ch-avoid-overmask-rem-small-obj.png
├── 21-avoid-overmask-green-ch-overmask-rem-small-obj.png
├── 21-avoid-overmask-green-ch.png
├── 21-overmask-green-ch-avoid-overmask-rem-small-obj.png
├── 21-overmask-green-ch-overmask-rem-small-obj.png
├── 21-overmask-green-ch.png
├── 21-rgb.png
├── 337-001.png
├── 337-002.png
├── 337-003.png
├── 337-004.png
├── 337-005.png
├── 337-006.png
├── 337-007.png
├── 337-008.png
├── 424-rgb.png
├── 498-rgb-after-filters.png
├── 498-rgb.png
├── 5-steps.png
├── TUPAC-TR-002-tile-r34-c34-x33793-y33799-w1024-h1024.png
├── TUPAC-TR-002-tile-r35-c37-x36865-y34823-w1024-h1024.png
├── adaptive-equalization.png
├── basic-threshold.png
├── binary-closing-20.png
├── binary-closing-5.png
├── binary-dilation-20.png
├── binary-dilation-5.png
├── binary-erosion-20.png
├── binary-erosion-5.png
├── binary-erosion-no-grays.png
├── binary-erosion-original.png
├── binary-opening-20.png
├── binary-opening-5.png
├── blue-filter.png
├── blue-original.png
├── blue-pen-filter.png
├── blue-pen.png
├── blue.png
├── canny-original-cropped.png
├── canny-original-with-inverse-mask.png
├── canny-original.png
├── canny.png
├── color-histograms-large-tile.png
├── color-histograms-small-tile.png
├── combine-pen-filters-no-blue-pen.png
├── combine-pen-filters-no-green-pen-no-blue-pen.png
├── combine-pen-filters-no-green-pen.png
├── combine-pen-filters-original-with-no-green-pen-no-blue-pen.png
├── combine-pen-filters-original.png
├── combine-pens-background-mask.png
├── combine-pens-background-original-with-inverse-mask.png
├── combine-pens-background-original-with-mask.png
├── combine-pens-background-original.png
├── complement.png
├── contrast-stretching.png
├── display-image-with-text.png
├── distribution-of-svs-image-sizes.png
├── entropy-grayscale.png
├── entropy-original-entropy-mask.png
├── entropy-original-inverse-entropy-mask.png
├── entropy-original.png
├── entropy.png
├── eosin-channel.png
├── fill-holes-remove-small-holes-100.png
├── fill-holes-remove-small-holes-10000.png
├── fill-holes.png
├── filter-example.png
├── filters-001-008.png
├── grays-filter.png
├── grayscale.png
├── green-channel-filter.png
├── green-filter.png
├── green-original.png
├── green-pen-filter.png
├── green-pen.png
├── green.png
├── hematoxylin-channel.png
├── histogram-equalization.png
├── hsv-hue-histogram.png
├── hysteresis-threshold.png
├── kmeans-original.png
├── kmeans-segmentation-after-otsu.png
├── kmeans-segmentation.png
├── not-blue-pen.png
├── not-blue.png
├── not-green-pen.png
├── not-green.png
├── not-red-pen.png
├── not-red.png
├── openslide-available-slides.png
├── openslide-whole-slide-image-zoomed.png
├── openslide-whole-slide-image.png
├── optional-tile-labels.png
├── otsu-mask.png
├── otsu-threshold.png
├── pink-and-purple-slide.png
├── purple-slide.png
├── rag-thresh-1.png
├── rag-thresh-20.png
├── rag-thresh-9.png
├── rag-thresh-original.png
├── red-filter.png
├── red-pen-filter.png
├── red-pen-slides-filters.png
├── red-pen.png
├── red.png
├── remove-more-green-more-gray.png
├── remove-small-holes-100.png
├── remove-small-holes-10000.png
├── remove-small-objects-100.png
├── remove-small-objects-10000.png
├── scoring-formula.png
├── slide-2-rgb-hsv.png
├── slide-2-row-25-col-30.png
├── slide-2-row-25-col-31.png
├── slide-2-row-25-col-32.png
├── slide-2-tile-tissue-heatmap-original.png
├── slide-2-tile-tissue-heatmap.png
├── slide-2-tissue-percentage-tile-1000.png
├── slide-2-tissue-percentage-tile-1500.png
├── slide-2-top-tile-borders.png
├── slide-2-top-tile-labels-borders.png
├── slide-2-top-tile-labels.png
├── slide-2-top-tiles-original.png
├── slide-2-top-tiles.png
├── slide-4-rgb.png
├── slide-4-top-tile-1.png
├── slide-4-top-tile-2.png
├── slide-pen.png
├── slide-scan.png
├── svs-image-sizes.png
├── tile-data.png
├── tiles-page.png
└── wsi-example.png
└── index.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | data/
3 | .DS_Store
4 | .idea/
5 | .project
6 | docs/_site
7 | **/__pycache__
8 | **/filters*.html
9 | **/tiles*.html
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
19 |
20 | # Python WSI Preprocessing
21 |
22 | This project contains a variety of files for investigating image preprocessing using Python
23 | with the aim of using deep learning to perform histopathology image classification of
24 | whole slide images.
25 |
26 | See main tutorial [here](./docs/wsi-preprocessing-in-python/index.md).
27 |
28 | See main project at [https://github.com/CODAIT/deep-histopath](https://github.com/CODAIT/deep-histopath)
29 | for more information.
30 |
--------------------------------------------------------------------------------
/deephistopath/wsi/filter.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | #
15 | # ------------------------------------------------------------------------
16 |
17 | import math
18 | import multiprocessing
19 | import numpy as np
20 | import os
21 | import scipy.ndimage.morphology as sc_morph
22 | import skimage.color as sk_color
23 | import skimage.exposure as sk_exposure
24 | import skimage.feature as sk_feature
25 | import skimage.filters as sk_filters
26 | import skimage.future as sk_future
27 | import skimage.morphology as sk_morphology
28 | import skimage.segmentation as sk_segmentation
29 |
30 | from deephistopath.wsi import slide
31 | from deephistopath.wsi import util
32 | from deephistopath.wsi.util import Time
33 |
34 |
35 | def filter_rgb_to_grayscale(np_img, output_type="uint8"):
36 | """
37 | Convert an RGB NumPy array to a grayscale NumPy array.
38 |
39 | Shape (h, w, c) to (h, w).
40 |
41 | Args:
42 | np_img: RGB Image as a NumPy array.
43 | output_type: Type of array to return (float or uint8)
44 |
45 | Returns:
46 | Grayscale image as NumPy array with shape (h, w).
47 | """
48 | t = Time()
49 | # Another common RGB ratio possibility: [0.299, 0.587, 0.114]
50 | grayscale = np.dot(np_img[..., :3], [0.2125, 0.7154, 0.0721])
51 | if output_type != "float":
52 | grayscale = grayscale.astype("uint8")
53 | util.np_info(grayscale, "Gray", t.elapsed())
54 | return grayscale
55 |
56 |
57 | def filter_complement(np_img, output_type="uint8"):
58 | """
59 | Obtain the complement of an image as a NumPy array.
60 |
61 | Args:
62 | np_img: Image as a NumPy array.
63 | type: Type of array to return (float or uint8).
64 |
65 | Returns:
66 | Complement image as Numpy array.
67 | """
68 | t = Time()
69 | if output_type == "float":
70 | complement = 1.0 - np_img
71 | else:
72 | complement = 255 - np_img
73 | util.np_info(complement, "Complement", t.elapsed())
74 | return complement
75 |
76 |
77 | def filter_hysteresis_threshold(np_img, low=50, high=100, output_type="uint8"):
78 | """
79 | Apply two-level (hysteresis) threshold to an image as a NumPy array, returning a binary image.
80 |
81 | Args:
82 | np_img: Image as a NumPy array.
83 | low: Low threshold.
84 | high: High threshold.
85 | output_type: Type of array to return (bool, float, or uint8).
86 |
87 | Returns:
88 | NumPy array (bool, float, or uint8) where True, 1.0, and 255 represent a pixel above hysteresis threshold.
89 | """
90 | t = Time()
91 | hyst = sk_filters.apply_hysteresis_threshold(np_img, low, high)
92 | if output_type == "bool":
93 | pass
94 | elif output_type == "float":
95 | hyst = hyst.astype(float)
96 | else:
97 | hyst = (255 * hyst).astype("uint8")
98 | util.np_info(hyst, "Hysteresis Threshold", t.elapsed())
99 | return hyst
100 |
101 |
102 | def filter_otsu_threshold(np_img, output_type="uint8"):
103 | """
104 | Compute Otsu threshold on image as a NumPy array and return binary image based on pixels above threshold.
105 |
106 | Args:
107 | np_img: Image as a NumPy array.
108 | output_type: Type of array to return (bool, float, or uint8).
109 |
110 | Returns:
111 | NumPy array (bool, float, or uint8) where True, 1.0, and 255 represent a pixel above Otsu threshold.
112 | """
113 | t = Time()
114 | otsu_thresh_value = sk_filters.threshold_otsu(np_img)
115 | otsu = (np_img > otsu_thresh_value)
116 | if output_type == "bool":
117 | pass
118 | elif output_type == "float":
119 | otsu = otsu.astype(float)
120 | else:
121 | otsu = otsu.astype("uint8") * 255
122 | util.np_info(otsu, "Otsu Threshold", t.elapsed())
123 | return otsu
124 |
125 |
126 | def filter_local_otsu_threshold(np_img, disk_size=3, output_type="uint8"):
127 | """
128 | Compute local Otsu threshold for each pixel and return binary image based on pixels being less than the
129 | local Otsu threshold.
130 |
131 | Args:
132 | np_img: Image as a NumPy array.
133 | disk_size: Radius of the disk structuring element used to compute the Otsu threshold for each pixel.
134 | output_type: Type of array to return (bool, float, or uint8).
135 |
136 | Returns:
137 | NumPy array (bool, float, or uint8) where local Otsu threshold values have been applied to original image.
138 | """
139 | t = Time()
140 | local_otsu = sk_filters.rank.otsu(np_img, sk_morphology.disk(disk_size))
141 | if output_type == "bool":
142 | pass
143 | elif output_type == "float":
144 | local_otsu = local_otsu.astype(float)
145 | else:
146 | local_otsu = local_otsu.astype("uint8") * 255
147 | util.np_info(local_otsu, "Otsu Local Threshold", t.elapsed())
148 | return local_otsu
149 |
150 |
151 | def filter_entropy(np_img, neighborhood=9, threshold=5, output_type="uint8"):
152 | """
153 | Filter image based on entropy (complexity).
154 |
155 | Args:
156 | np_img: Image as a NumPy array.
157 | neighborhood: Neighborhood size (defines height and width of 2D array of 1's).
158 | threshold: Threshold value.
159 | output_type: Type of array to return (bool, float, or uint8).
160 |
161 | Returns:
162 | NumPy array (bool, float, or uint8) where True, 1.0, and 255 represent a measure of complexity.
163 | """
164 | t = Time()
165 | entr = sk_filters.rank.entropy(np_img, np.ones((neighborhood, neighborhood))) > threshold
166 | if output_type == "bool":
167 | pass
168 | elif output_type == "float":
169 | entr = entr.astype(float)
170 | else:
171 | entr = entr.astype("uint8") * 255
172 | util.np_info(entr, "Entropy", t.elapsed())
173 | return entr
174 |
175 |
176 | def filter_canny(np_img, sigma=1, low_threshold=0, high_threshold=25, output_type="uint8"):
177 | """
178 | Filter image based on Canny algorithm edges.
179 |
180 | Args:
181 | np_img: Image as a NumPy array.
182 | sigma: Width (std dev) of Gaussian.
183 | low_threshold: Low hysteresis threshold value.
184 | high_threshold: High hysteresis threshold value.
185 | output_type: Type of array to return (bool, float, or uint8).
186 |
187 | Returns:
188 | NumPy array (bool, float, or uint8) representing Canny edge map (binary image).
189 | """
190 | t = Time()
191 | can = sk_feature.canny(np_img, sigma=sigma, low_threshold=low_threshold, high_threshold=high_threshold)
192 | if output_type == "bool":
193 | pass
194 | elif output_type == "float":
195 | can = can.astype(float)
196 | else:
197 | can = can.astype("uint8") * 255
198 | util.np_info(can, "Canny Edges", t.elapsed())
199 | return can
200 |
201 |
202 | def mask_percent(np_img):
203 | """
204 | Determine the percentage of a NumPy array that is masked (how many of the values are 0 values).
205 |
206 | Args:
207 | np_img: Image as a NumPy array.
208 |
209 | Returns:
210 | The percentage of the NumPy array that is masked.
211 | """
212 | if (len(np_img.shape) == 3) and (np_img.shape[2] == 3):
213 | np_sum = np_img[:, :, 0] + np_img[:, :, 1] + np_img[:, :, 2]
214 | mask_percentage = 100 - np.count_nonzero(np_sum) / np_sum.size * 100
215 | else:
216 | mask_percentage = 100 - np.count_nonzero(np_img) / np_img.size * 100
217 | return mask_percentage
218 |
219 |
220 | def tissue_percent(np_img):
221 | """
222 | Determine the percentage of a NumPy array that is tissue (not masked).
223 |
224 | Args:
225 | np_img: Image as a NumPy array.
226 |
227 | Returns:
228 | The percentage of the NumPy array that is tissue.
229 | """
230 | return 100 - mask_percent(np_img)
231 |
232 |
233 | def filter_remove_small_objects(np_img, min_size=3000, avoid_overmask=True, overmask_thresh=95, output_type="uint8"):
234 | """
235 | Filter image to remove small objects (connected components) less than a particular minimum size. If avoid_overmask
236 | is True, this function can recursively call itself with progressively smaller minimum size objects to remove to
237 | reduce the amount of masking that this filter performs.
238 |
239 | Args:
240 | np_img: Image as a NumPy array of type bool.
241 | min_size: Minimum size of small object to remove.
242 | avoid_overmask: If True, avoid masking above the overmask_thresh percentage.
243 | overmask_thresh: If avoid_overmask is True, avoid masking above this threshold percentage value.
244 | output_type: Type of array to return (bool, float, or uint8).
245 |
246 | Returns:
247 | NumPy array (bool, float, or uint8).
248 | """
249 | t = Time()
250 |
251 | rem_sm = np_img.astype(bool) # make sure mask is boolean
252 | rem_sm = sk_morphology.remove_small_objects(rem_sm, min_size=min_size)
253 | mask_percentage = mask_percent(rem_sm)
254 | if (mask_percentage >= overmask_thresh) and (min_size >= 1) and (avoid_overmask is True):
255 | new_min_size = min_size / 2
256 | print("Mask percentage %3.2f%% >= overmask threshold %3.2f%% for Remove Small Objs size %d, so try %d" % (
257 | mask_percentage, overmask_thresh, min_size, new_min_size))
258 | rem_sm = filter_remove_small_objects(np_img, new_min_size, avoid_overmask, overmask_thresh, output_type)
259 | np_img = rem_sm
260 |
261 | if output_type == "bool":
262 | pass
263 | elif output_type == "float":
264 | np_img = np_img.astype(float)
265 | else:
266 | np_img = np_img.astype("uint8") * 255
267 |
268 | util.np_info(np_img, "Remove Small Objs", t.elapsed())
269 | return np_img
270 |
271 |
272 | def filter_remove_small_holes(np_img, min_size=3000, output_type="uint8"):
273 | """
274 | Filter image to remove small holes less than a particular size.
275 |
276 | Args:
277 | np_img: Image as a NumPy array of type bool.
278 | min_size: Remove small holes below this size.
279 | output_type: Type of array to return (bool, float, or uint8).
280 |
281 | Returns:
282 | NumPy array (bool, float, or uint8).
283 | """
284 | t = Time()
285 |
286 | rem_sm = sk_morphology.remove_small_holes(np_img, min_size=min_size)
287 |
288 | if output_type == "bool":
289 | pass
290 | elif output_type == "float":
291 | rem_sm = rem_sm.astype(float)
292 | else:
293 | rem_sm = rem_sm.astype("uint8") * 255
294 |
295 | util.np_info(rem_sm, "Remove Small Holes", t.elapsed())
296 | return rem_sm
297 |
298 |
299 | def filter_contrast_stretch(np_img, low=40, high=60):
300 | """
301 | Filter image (gray or RGB) using contrast stretching to increase contrast in image based on the intensities in
302 | a specified range.
303 |
304 | Args:
305 | np_img: Image as a NumPy array (gray or RGB).
306 | low: Range low value (0 to 255).
307 | high: Range high value (0 to 255).
308 |
309 | Returns:
310 | Image as NumPy array with contrast enhanced.
311 | """
312 | t = Time()
313 | low_p, high_p = np.percentile(np_img, (low * 100 / 255, high * 100 / 255))
314 | contrast_stretch = sk_exposure.rescale_intensity(np_img, in_range=(low_p, high_p))
315 | util.np_info(contrast_stretch, "Contrast Stretch", t.elapsed())
316 | return contrast_stretch
317 |
318 |
319 | def filter_histogram_equalization(np_img, nbins=256, output_type="uint8"):
320 | """
321 | Filter image (gray or RGB) using histogram equalization to increase contrast in image.
322 |
323 | Args:
324 | np_img: Image as a NumPy array (gray or RGB).
325 | nbins: Number of histogram bins.
326 | output_type: Type of array to return (float or uint8).
327 |
328 | Returns:
329 | NumPy array (float or uint8) with contrast enhanced by histogram equalization.
330 | """
331 | t = Time()
332 | # if uint8 type and nbins is specified, convert to float so that nbins can be a value besides 256
333 | if np_img.dtype == "uint8" and nbins != 256:
334 | np_img = np_img / 255
335 | hist_equ = sk_exposure.equalize_hist(np_img, nbins=nbins)
336 | if output_type == "float":
337 | pass
338 | else:
339 | hist_equ = (hist_equ * 255).astype("uint8")
340 | util.np_info(hist_equ, "Hist Equalization", t.elapsed())
341 | return hist_equ
342 |
343 |
344 | def filter_adaptive_equalization(np_img, nbins=256, clip_limit=0.01, output_type="uint8"):
345 | """
346 | Filter image (gray or RGB) using adaptive equalization to increase contrast in image, where contrast in local regions
347 | is enhanced.
348 |
349 | Args:
350 | np_img: Image as a NumPy array (gray or RGB).
351 | nbins: Number of histogram bins.
352 | clip_limit: Clipping limit where higher value increases contrast.
353 | output_type: Type of array to return (float or uint8).
354 |
355 | Returns:
356 | NumPy array (float or uint8) with contrast enhanced by adaptive equalization.
357 | """
358 | t = Time()
359 | adapt_equ = sk_exposure.equalize_adapthist(np_img, nbins=nbins, clip_limit=clip_limit)
360 | if output_type == "float":
361 | pass
362 | else:
363 | adapt_equ = (adapt_equ * 255).astype("uint8")
364 | util.np_info(adapt_equ, "Adapt Equalization", t.elapsed())
365 | return adapt_equ
366 |
367 |
368 | def filter_local_equalization(np_img, disk_size=50):
369 | """
370 | Filter image (gray) using local equalization, which uses local histograms based on the disk structuring element.
371 |
372 | Args:
373 | np_img: Image as a NumPy array.
374 | disk_size: Radius of the disk structuring element used for the local histograms
375 |
376 | Returns:
377 | NumPy array with contrast enhanced using local equalization.
378 | """
379 | t = Time()
380 | local_equ = sk_filters.rank.equalize(np_img, selem=sk_morphology.disk(disk_size))
381 | util.np_info(local_equ, "Local Equalization", t.elapsed())
382 | return local_equ
383 |
384 |
385 | def filter_rgb_to_hed(np_img, output_type="uint8"):
386 | """
387 | Filter RGB channels to HED (Hematoxylin - Eosin - Diaminobenzidine) channels.
388 |
389 | Args:
390 | np_img: RGB image as a NumPy array.
391 | output_type: Type of array to return (float or uint8).
392 |
393 | Returns:
394 | NumPy array (float or uint8) with HED channels.
395 | """
396 | t = Time()
397 | hed = sk_color.rgb2hed(np_img)
398 | if output_type == "float":
399 | hed = sk_exposure.rescale_intensity(hed, out_range=(0.0, 1.0))
400 | else:
401 | hed = (sk_exposure.rescale_intensity(hed, out_range=(0, 255))).astype("uint8")
402 |
403 | util.np_info(hed, "RGB to HED", t.elapsed())
404 | return hed
405 |
406 |
407 | def filter_rgb_to_hsv(np_img, display_np_info=True):
408 | """
409 | Filter RGB channels to HSV (Hue, Saturation, Value).
410 |
411 | Args:
412 | np_img: RGB image as a NumPy array.
413 | display_np_info: If True, display NumPy array info and filter time.
414 |
415 | Returns:
416 | Image as NumPy array in HSV representation.
417 | """
418 |
419 | if display_np_info:
420 | t = Time()
421 | hsv = sk_color.rgb2hsv(np_img)
422 | if display_np_info:
423 | util.np_info(hsv, "RGB to HSV", t.elapsed())
424 | return hsv
425 |
426 |
427 | def filter_hsv_to_h(hsv, output_type="int", display_np_info=True):
428 | """
429 | Obtain hue values from HSV NumPy array as a 1-dimensional array. If output as an int array, the original float
430 | values are multiplied by 360 for their degree equivalents for simplicity. For more information, see
431 | https://en.wikipedia.org/wiki/HSL_and_HSV
432 |
433 | Args:
434 | hsv: HSV image as a NumPy array.
435 | output_type: Type of array to return (float or int).
436 | display_np_info: If True, display NumPy array info and filter time.
437 |
438 | Returns:
439 | Hue values (float or int) as a 1-dimensional NumPy array.
440 | """
441 | if display_np_info:
442 | t = Time()
443 | h = hsv[:, :, 0]
444 | h = h.flatten()
445 | if output_type == "int":
446 | h *= 360
447 | h = h.astype("int")
448 | if display_np_info:
449 | util.np_info(hsv, "HSV to H", t.elapsed())
450 | return h
451 |
452 |
453 | def filter_hsv_to_s(hsv):
454 | """
455 | Experimental HSV to S (saturation).
456 |
457 | Args:
458 | hsv: HSV image as a NumPy array.
459 |
460 | Returns:
461 | Saturation values as a 1-dimensional NumPy array.
462 | """
463 | s = hsv[:, :, 1]
464 | s = s.flatten()
465 | return s
466 |
467 |
468 | def filter_hsv_to_v(hsv):
469 | """
470 | Experimental HSV to V (value).
471 |
472 | Args:
473 | hsv: HSV image as a NumPy array.
474 |
475 | Returns:
476 | Value values as a 1-dimensional NumPy array.
477 | """
478 | v = hsv[:, :, 2]
479 | v = v.flatten()
480 | return v
481 |
482 |
483 | def filter_hed_to_hematoxylin(np_img, output_type="uint8"):
484 | """
485 | Obtain Hematoxylin channel from HED NumPy array and rescale it (for example, to 0 to 255 for uint8) for increased
486 | contrast.
487 |
488 | Args:
489 | np_img: HED image as a NumPy array.
490 | output_type: Type of array to return (float or uint8).
491 |
492 | Returns:
493 | NumPy array for Hematoxylin channel.
494 | """
495 | t = Time()
496 | hema = np_img[:, :, 0]
497 | if output_type == "float":
498 | hema = sk_exposure.rescale_intensity(hema, out_range=(0.0, 1.0))
499 | else:
500 | hema = (sk_exposure.rescale_intensity(hema, out_range=(0, 255))).astype("uint8")
501 | util.np_info(hema, "HED to Hematoxylin", t.elapsed())
502 | return hema
503 |
504 |
505 | def filter_hed_to_eosin(np_img, output_type="uint8"):
506 | """
507 | Obtain Eosin channel from HED NumPy array and rescale it (for example, to 0 to 255 for uint8) for increased
508 | contrast.
509 |
510 | Args:
511 | np_img: HED image as a NumPy array.
512 | output_type: Type of array to return (float or uint8).
513 |
514 | Returns:
515 | NumPy array for Eosin channel.
516 | """
517 | t = Time()
518 | eosin = np_img[:, :, 1]
519 | if output_type == "float":
520 | eosin = sk_exposure.rescale_intensity(eosin, out_range=(0.0, 1.0))
521 | else:
522 | eosin = (sk_exposure.rescale_intensity(eosin, out_range=(0, 255))).astype("uint8")
523 | util.np_info(eosin, "HED to Eosin", t.elapsed())
524 | return eosin
525 |
526 |
527 | def filter_binary_fill_holes(np_img, output_type="bool"):
528 | """
529 | Fill holes in a binary object (bool, float, or uint8).
530 |
531 | Args:
532 | np_img: Binary image as a NumPy array.
533 | output_type: Type of array to return (bool, float, or uint8).
534 |
535 | Returns:
536 | NumPy array (bool, float, or uint8) where holes have been filled.
537 | """
538 | t = Time()
539 | if np_img.dtype == "uint8":
540 | np_img = np_img / 255
541 | result = sc_morph.binary_fill_holes(np_img)
542 | if output_type == "bool":
543 | pass
544 | elif output_type == "float":
545 | result = result.astype(float)
546 | else:
547 | result = result.astype("uint8") * 255
548 | util.np_info(result, "Binary Fill Holes", t.elapsed())
549 | return result
550 |
551 |
552 | def filter_binary_erosion(np_img, disk_size=5, iterations=1, output_type="uint8"):
553 | """
554 | Erode a binary object (bool, float, or uint8).
555 |
556 | Args:
557 | np_img: Binary image as a NumPy array.
558 | disk_size: Radius of the disk structuring element used for erosion.
559 | iterations: How many times to repeat the erosion.
560 | output_type: Type of array to return (bool, float, or uint8).
561 |
562 | Returns:
563 | NumPy array (bool, float, or uint8) where edges have been eroded.
564 | """
565 | t = Time()
566 | if np_img.dtype == "uint8":
567 | np_img = np_img / 255
568 | result = sc_morph.binary_erosion(np_img, sk_morphology.disk(disk_size), iterations=iterations)
569 | if output_type == "bool":
570 | pass
571 | elif output_type == "float":
572 | result = result.astype(float)
573 | else:
574 | result = result.astype("uint8") * 255
575 | util.np_info(result, "Binary Erosion", t.elapsed())
576 | return result
577 |
578 |
579 | def filter_binary_dilation(np_img, disk_size=5, iterations=1, output_type="uint8"):
580 | """
581 | Dilate a binary object (bool, float, or uint8).
582 |
583 | Args:
584 | np_img: Binary image as a NumPy array.
585 | disk_size: Radius of the disk structuring element used for dilation.
586 | iterations: How many times to repeat the dilation.
587 | output_type: Type of array to return (bool, float, or uint8).
588 |
589 | Returns:
590 | NumPy array (bool, float, or uint8) where edges have been dilated.
591 | """
592 | t = Time()
593 | if np_img.dtype == "uint8":
594 | np_img = np_img / 255
595 | result = sc_morph.binary_dilation(np_img, sk_morphology.disk(disk_size), iterations=iterations)
596 | if output_type == "bool":
597 | pass
598 | elif output_type == "float":
599 | result = result.astype(float)
600 | else:
601 | result = result.astype("uint8") * 255
602 | util.np_info(result, "Binary Dilation", t.elapsed())
603 | return result
604 |
605 |
606 | def filter_binary_opening(np_img, disk_size=3, iterations=1, output_type="uint8"):
607 | """
608 | Open a binary object (bool, float, or uint8). Opening is an erosion followed by a dilation.
609 | Opening can be used to remove small objects.
610 |
611 | Args:
612 | np_img: Binary image as a NumPy array.
613 | disk_size: Radius of the disk structuring element used for opening.
614 | iterations: How many times to repeat.
615 | output_type: Type of array to return (bool, float, or uint8).
616 |
617 | Returns:
618 | NumPy array (bool, float, or uint8) following binary opening.
619 | """
620 | t = Time()
621 | if np_img.dtype == "uint8":
622 | np_img = np_img / 255
623 | result = sc_morph.binary_opening(np_img, sk_morphology.disk(disk_size), iterations=iterations)
624 | if output_type == "bool":
625 | pass
626 | elif output_type == "float":
627 | result = result.astype(float)
628 | else:
629 | result = result.astype("uint8") * 255
630 | util.np_info(result, "Binary Opening", t.elapsed())
631 | return result
632 |
633 |
634 | def filter_binary_closing(np_img, disk_size=3, iterations=1, output_type="uint8"):
635 | """
636 | Close a binary object (bool, float, or uint8). Closing is a dilation followed by an erosion.
637 | Closing can be used to remove small holes.
638 |
639 | Args:
640 | np_img: Binary image as a NumPy array.
641 | disk_size: Radius of the disk structuring element used for closing.
642 | iterations: How many times to repeat.
643 | output_type: Type of array to return (bool, float, or uint8).
644 |
645 | Returns:
646 | NumPy array (bool, float, or uint8) following binary closing.
647 | """
648 | t = Time()
649 | if np_img.dtype == "uint8":
650 | np_img = np_img / 255
651 | result = sc_morph.binary_closing(np_img, sk_morphology.disk(disk_size), iterations=iterations)
652 | if output_type == "bool":
653 | pass
654 | elif output_type == "float":
655 | result = result.astype(float)
656 | else:
657 | result = result.astype("uint8") * 255
658 | util.np_info(result, "Binary Closing", t.elapsed())
659 | return result
660 |
661 |
662 | def filter_kmeans_segmentation(np_img, compactness=10, n_segments=800):
663 | """
664 | Use K-means segmentation (color/space proximity) to segment RGB image where each segment is
665 | colored based on the average color for that segment.
666 |
667 | Args:
668 | np_img: Binary image as a NumPy array.
669 | compactness: Color proximity versus space proximity factor.
670 | n_segments: The number of segments.
671 |
672 | Returns:
673 | NumPy array (uint8) representing 3-channel RGB image where each segment has been colored based on the average
674 | color for that segment.
675 | """
676 | t = Time()
677 | labels = sk_segmentation.slic(np_img, compactness=compactness, n_segments=n_segments)
678 | result = sk_color.label2rgb(labels, np_img, kind='avg')
679 | util.np_info(result, "K-Means Segmentation", t.elapsed())
680 | return result
681 |
682 |
683 | def filter_rag_threshold(np_img, compactness=10, n_segments=800, threshold=9):
684 | """
685 | Use K-means segmentation to segment RGB image, build region adjacency graph based on the segments, combine
686 | similar regions based on threshold value, and then output these resulting region segments.
687 |
688 | Args:
689 | np_img: Binary image as a NumPy array.
690 | compactness: Color proximity versus space proximity factor.
691 | n_segments: The number of segments.
692 | threshold: Threshold value for combining regions.
693 |
694 | Returns:
695 | NumPy array (uint8) representing 3-channel RGB image where each segment has been colored based on the average
696 | color for that segment (and similar segments have been combined).
697 | """
698 | t = Time()
699 | labels = sk_segmentation.slic(np_img, compactness=compactness, n_segments=n_segments)
700 | g = sk_future.graph.rag_mean_color(np_img, labels)
701 | labels2 = sk_future.graph.cut_threshold(labels, g, threshold)
702 | result = sk_color.label2rgb(labels2, np_img, kind='avg')
703 | util.np_info(result, "RAG Threshold", t.elapsed())
704 | return result
705 |
706 |
707 | def filter_threshold(np_img, threshold, output_type="bool"):
708 | """
709 | Return mask where a pixel has a value if it exceeds the threshold value.
710 |
711 | Args:
712 | np_img: Binary image as a NumPy array.
713 | threshold: The threshold value to exceed.
714 | output_type: Type of array to return (bool, float, or uint8).
715 |
716 | Returns:
717 | NumPy array representing a mask where a pixel has a value (T, 1.0, or 255) if the corresponding input array
718 | pixel exceeds the threshold value.
719 | """
720 | t = Time()
721 | result = (np_img > threshold)
722 | if output_type == "bool":
723 | pass
724 | elif output_type == "float":
725 | result = result.astype(float)
726 | else:
727 | result = result.astype("uint8") * 255
728 | util.np_info(result, "Threshold", t.elapsed())
729 | return result
730 |
731 |
732 | def filter_green_channel(np_img, green_thresh=200, avoid_overmask=True, overmask_thresh=90, output_type="bool"):
733 | """
734 | Create a mask to filter out pixels with a green channel value greater than a particular threshold, since hematoxylin
735 | and eosin are purplish and pinkish, which do not have much green to them.
736 |
737 | Args:
738 | np_img: RGB image as a NumPy array.
739 | green_thresh: Green channel threshold value (0 to 255). If value is greater than green_thresh, mask out pixel.
740 | avoid_overmask: If True, avoid masking above the overmask_thresh percentage.
741 | overmask_thresh: If avoid_overmask is True, avoid masking above this threshold percentage value.
742 | output_type: Type of array to return (bool, float, or uint8).
743 |
744 | Returns:
745 | NumPy array representing a mask where pixels above a particular green channel threshold have been masked out.
746 | """
747 | t = Time()
748 |
749 | g = np_img[:, :, 1]
750 | gr_ch_mask = (g < green_thresh) & (g > 0)
751 | mask_percentage = mask_percent(gr_ch_mask)
752 | if (mask_percentage >= overmask_thresh) and (green_thresh < 255) and (avoid_overmask is True):
753 | new_green_thresh = math.ceil((255 - green_thresh) / 2 + green_thresh)
754 | print(
755 | "Mask percentage %3.2f%% >= overmask threshold %3.2f%% for Remove Green Channel green_thresh=%d, so try %d" % (
756 | mask_percentage, overmask_thresh, green_thresh, new_green_thresh))
757 | gr_ch_mask = filter_green_channel(np_img, new_green_thresh, avoid_overmask, overmask_thresh, output_type)
758 | np_img = gr_ch_mask
759 |
760 | if output_type == "bool":
761 | pass
762 | elif output_type == "float":
763 | np_img = np_img.astype(float)
764 | else:
765 | np_img = np_img.astype("uint8") * 255
766 |
767 | util.np_info(np_img, "Filter Green Channel", t.elapsed())
768 | return np_img
769 |
770 |
771 | def filter_red(rgb, red_lower_thresh, green_upper_thresh, blue_upper_thresh, output_type="bool",
772 | display_np_info=False):
773 | """
774 | Create a mask to filter out reddish colors, where the mask is based on a pixel being above a
775 | red channel threshold value, below a green channel threshold value, and below a blue channel threshold value.
776 |
777 | Args:
778 | rgb: RGB image as a NumPy array.
779 | red_lower_thresh: Red channel lower threshold value.
780 | green_upper_thresh: Green channel upper threshold value.
781 | blue_upper_thresh: Blue channel upper threshold value.
782 | output_type: Type of array to return (bool, float, or uint8).
783 | display_np_info: If True, display NumPy array info and filter time.
784 |
785 | Returns:
786 | NumPy array representing the mask.
787 | """
788 | if display_np_info:
789 | t = Time()
790 | r = rgb[:, :, 0] > red_lower_thresh
791 | g = rgb[:, :, 1] < green_upper_thresh
792 | b = rgb[:, :, 2] < blue_upper_thresh
793 | result = ~(r & g & b)
794 | if output_type == "bool":
795 | pass
796 | elif output_type == "float":
797 | result = result.astype(float)
798 | else:
799 | result = result.astype("uint8") * 255
800 | if display_np_info:
801 | util.np_info(result, "Filter Red", t.elapsed())
802 | return result
803 |
804 |
805 | def filter_red_pen(rgb, output_type="bool"):
806 | """
807 | Create a mask to filter out red pen marks from a slide.
808 |
809 | Args:
810 | rgb: RGB image as a NumPy array.
811 | output_type: Type of array to return (bool, float, or uint8).
812 |
813 | Returns:
814 | NumPy array representing the mask.
815 | """
816 | t = Time()
817 | result = filter_red(rgb, red_lower_thresh=150, green_upper_thresh=80, blue_upper_thresh=90) & \
818 | filter_red(rgb, red_lower_thresh=110, green_upper_thresh=20, blue_upper_thresh=30) & \
819 | filter_red(rgb, red_lower_thresh=185, green_upper_thresh=65, blue_upper_thresh=105) & \
820 | filter_red(rgb, red_lower_thresh=195, green_upper_thresh=85, blue_upper_thresh=125) & \
821 | filter_red(rgb, red_lower_thresh=220, green_upper_thresh=115, blue_upper_thresh=145) & \
822 | filter_red(rgb, red_lower_thresh=125, green_upper_thresh=40, blue_upper_thresh=70) & \
823 | filter_red(rgb, red_lower_thresh=200, green_upper_thresh=120, blue_upper_thresh=150) & \
824 | filter_red(rgb, red_lower_thresh=100, green_upper_thresh=50, blue_upper_thresh=65) & \
825 | filter_red(rgb, red_lower_thresh=85, green_upper_thresh=25, blue_upper_thresh=45)
826 | if output_type == "bool":
827 | pass
828 | elif output_type == "float":
829 | result = result.astype(float)
830 | else:
831 | result = result.astype("uint8") * 255
832 | util.np_info(result, "Filter Red Pen", t.elapsed())
833 | return result
834 |
835 |
836 | def filter_green(rgb, red_upper_thresh, green_lower_thresh, blue_lower_thresh, output_type="bool",
837 | display_np_info=False):
838 | """
839 | Create a mask to filter out greenish colors, where the mask is based on a pixel being below a
840 | red channel threshold value, above a green channel threshold value, and above a blue channel threshold value.
841 | Note that for the green ink, the green and blue channels tend to track together, so we use a blue channel
842 | lower threshold value rather than a blue channel upper threshold value.
843 |
844 | Args:
845 | rgb: RGB image as a NumPy array.
846 | red_upper_thresh: Red channel upper threshold value.
847 | green_lower_thresh: Green channel lower threshold value.
848 | blue_lower_thresh: Blue channel lower threshold value.
849 | output_type: Type of array to return (bool, float, or uint8).
850 | display_np_info: If True, display NumPy array info and filter time.
851 |
852 | Returns:
853 | NumPy array representing the mask.
854 | """
855 | if display_np_info:
856 | t = Time()
857 | r = rgb[:, :, 0] < red_upper_thresh
858 | g = rgb[:, :, 1] > green_lower_thresh
859 | b = rgb[:, :, 2] > blue_lower_thresh
860 | result = ~(r & g & b)
861 | if output_type == "bool":
862 | pass
863 | elif output_type == "float":
864 | result = result.astype(float)
865 | else:
866 | result = result.astype("uint8") * 255
867 | if display_np_info:
868 | util.np_info(result, "Filter Green", t.elapsed())
869 | return result
870 |
871 |
872 | def filter_green_pen(rgb, output_type="bool"):
873 | """
874 | Create a mask to filter out green pen marks from a slide.
875 |
876 | Args:
877 | rgb: RGB image as a NumPy array.
878 | output_type: Type of array to return (bool, float, or uint8).
879 |
880 | Returns:
881 | NumPy array representing the mask.
882 | """
883 | t = Time()
884 | result = filter_green(rgb, red_upper_thresh=150, green_lower_thresh=160, blue_lower_thresh=140) & \
885 | filter_green(rgb, red_upper_thresh=70, green_lower_thresh=110, blue_lower_thresh=110) & \
886 | filter_green(rgb, red_upper_thresh=45, green_lower_thresh=115, blue_lower_thresh=100) & \
887 | filter_green(rgb, red_upper_thresh=30, green_lower_thresh=75, blue_lower_thresh=60) & \
888 | filter_green(rgb, red_upper_thresh=195, green_lower_thresh=220, blue_lower_thresh=210) & \
889 | filter_green(rgb, red_upper_thresh=225, green_lower_thresh=230, blue_lower_thresh=225) & \
890 | filter_green(rgb, red_upper_thresh=170, green_lower_thresh=210, blue_lower_thresh=200) & \
891 | filter_green(rgb, red_upper_thresh=20, green_lower_thresh=30, blue_lower_thresh=20) & \
892 | filter_green(rgb, red_upper_thresh=50, green_lower_thresh=60, blue_lower_thresh=40) & \
893 | filter_green(rgb, red_upper_thresh=30, green_lower_thresh=50, blue_lower_thresh=35) & \
894 | filter_green(rgb, red_upper_thresh=65, green_lower_thresh=70, blue_lower_thresh=60) & \
895 | filter_green(rgb, red_upper_thresh=100, green_lower_thresh=110, blue_lower_thresh=105) & \
896 | filter_green(rgb, red_upper_thresh=165, green_lower_thresh=180, blue_lower_thresh=180) & \
897 | filter_green(rgb, red_upper_thresh=140, green_lower_thresh=140, blue_lower_thresh=150) & \
898 | filter_green(rgb, red_upper_thresh=185, green_lower_thresh=195, blue_lower_thresh=195)
899 | if output_type == "bool":
900 | pass
901 | elif output_type == "float":
902 | result = result.astype(float)
903 | else:
904 | result = result.astype("uint8") * 255
905 | util.np_info(result, "Filter Green Pen", t.elapsed())
906 | return result
907 |
908 |
909 | def filter_blue(rgb, red_upper_thresh, green_upper_thresh, blue_lower_thresh, output_type="bool",
910 | display_np_info=False):
911 | """
912 | Create a mask to filter out blueish colors, where the mask is based on a pixel being below a
913 | red channel threshold value, below a green channel threshold value, and above a blue channel threshold value.
914 |
915 | Args:
916 | rgb: RGB image as a NumPy array.
917 | red_upper_thresh: Red channel upper threshold value.
918 | green_upper_thresh: Green channel upper threshold value.
919 | blue_lower_thresh: Blue channel lower threshold value.
920 | output_type: Type of array to return (bool, float, or uint8).
921 | display_np_info: If True, display NumPy array info and filter time.
922 |
923 | Returns:
924 | NumPy array representing the mask.
925 | """
926 | if display_np_info:
927 | t = Time()
928 | r = rgb[:, :, 0] < red_upper_thresh
929 | g = rgb[:, :, 1] < green_upper_thresh
930 | b = rgb[:, :, 2] > blue_lower_thresh
931 | result = ~(r & g & b)
932 | if output_type == "bool":
933 | pass
934 | elif output_type == "float":
935 | result = result.astype(float)
936 | else:
937 | result = result.astype("uint8") * 255
938 | if display_np_info:
939 | util.np_info(result, "Filter Blue", t.elapsed())
940 | return result
941 |
942 |
943 | def filter_blue_pen(rgb, output_type="bool"):
944 | """
945 | Create a mask to filter out blue pen marks from a slide.
946 |
947 | Args:
948 | rgb: RGB image as a NumPy array.
949 | output_type: Type of array to return (bool, float, or uint8).
950 |
951 | Returns:
952 | NumPy array representing the mask.
953 | """
954 | t = Time()
955 | result = filter_blue(rgb, red_upper_thresh=60, green_upper_thresh=120, blue_lower_thresh=190) & \
956 | filter_blue(rgb, red_upper_thresh=120, green_upper_thresh=170, blue_lower_thresh=200) & \
957 | filter_blue(rgb, red_upper_thresh=175, green_upper_thresh=210, blue_lower_thresh=230) & \
958 | filter_blue(rgb, red_upper_thresh=145, green_upper_thresh=180, blue_lower_thresh=210) & \
959 | filter_blue(rgb, red_upper_thresh=37, green_upper_thresh=95, blue_lower_thresh=160) & \
960 | filter_blue(rgb, red_upper_thresh=30, green_upper_thresh=65, blue_lower_thresh=130) & \
961 | filter_blue(rgb, red_upper_thresh=130, green_upper_thresh=155, blue_lower_thresh=180) & \
962 | filter_blue(rgb, red_upper_thresh=40, green_upper_thresh=35, blue_lower_thresh=85) & \
963 | filter_blue(rgb, red_upper_thresh=30, green_upper_thresh=20, blue_lower_thresh=65) & \
964 | filter_blue(rgb, red_upper_thresh=90, green_upper_thresh=90, blue_lower_thresh=140) & \
965 | filter_blue(rgb, red_upper_thresh=60, green_upper_thresh=60, blue_lower_thresh=120) & \
966 | filter_blue(rgb, red_upper_thresh=110, green_upper_thresh=110, blue_lower_thresh=175)
967 | if output_type == "bool":
968 | pass
969 | elif output_type == "float":
970 | result = result.astype(float)
971 | else:
972 | result = result.astype("uint8") * 255
973 | util.np_info(result, "Filter Blue Pen", t.elapsed())
974 | return result
975 |
976 |
977 | def filter_grays(rgb, tolerance=15, output_type="bool"):
978 | """
979 | Create a mask to filter out pixels where the red, green, and blue channel values are similar.
980 |
981 | Args:
982 | np_img: RGB image as a NumPy array.
983 | tolerance: Tolerance value to determine how similar the values must be in order to be filtered out
984 | output_type: Type of array to return (bool, float, or uint8).
985 |
986 | Returns:
987 | NumPy array representing a mask where pixels with similar red, green, and blue values have been masked out.
988 | """
989 | t = Time()
990 | (h, w, c) = rgb.shape
991 |
992 | rgb = rgb.astype(np.int)
993 | rg_diff = abs(rgb[:, :, 0] - rgb[:, :, 1]) <= tolerance
994 | rb_diff = abs(rgb[:, :, 0] - rgb[:, :, 2]) <= tolerance
995 | gb_diff = abs(rgb[:, :, 1] - rgb[:, :, 2]) <= tolerance
996 | result = ~(rg_diff & rb_diff & gb_diff)
997 |
998 | if output_type == "bool":
999 | pass
1000 | elif output_type == "float":
1001 | result = result.astype(float)
1002 | else:
1003 | result = result.astype("uint8") * 255
1004 | util.np_info(result, "Filter Grays", t.elapsed())
1005 | return result
1006 |
1007 |
1008 | def uint8_to_bool(np_img):
1009 | """
1010 | Convert NumPy array of uint8 (255,0) values to bool (True,False) values
1011 |
1012 | Args:
1013 | np_img: Binary image as NumPy array of uint8 (255,0) values.
1014 |
1015 | Returns:
1016 | NumPy array of bool (True,False) values.
1017 | """
1018 | result = (np_img / 255).astype(bool)
1019 | return result
1020 |
1021 |
1022 | def apply_image_filters(np_img, slide_num=None, info=None, save=False, display=False):
1023 | """
1024 | Apply filters to image as NumPy array and optionally save and/or display filtered images.
1025 |
1026 | Args:
1027 | np_img: Image as NumPy array.
1028 | slide_num: The slide number (used for saving/displaying).
1029 | info: Dictionary of slide information (used for HTML display).
1030 | save: If True, save image.
1031 | display: If True, display image.
1032 |
1033 | Returns:
1034 | Resulting filtered image as a NumPy array.
1035 | """
1036 | rgb = np_img
1037 | save_display(save, display, info, rgb, slide_num, 1, "Original", "rgb")
1038 |
1039 | mask_not_green = filter_green_channel(rgb)
1040 | rgb_not_green = util.mask_rgb(rgb, mask_not_green)
1041 | save_display(save, display, info, rgb_not_green, slide_num, 2, "Not Green", "rgb-not-green")
1042 |
1043 | mask_not_gray = filter_grays(rgb)
1044 | rgb_not_gray = util.mask_rgb(rgb, mask_not_gray)
1045 | save_display(save, display, info, rgb_not_gray, slide_num, 3, "Not Gray", "rgb-not-gray")
1046 |
1047 | mask_no_red_pen = filter_red_pen(rgb)
1048 | rgb_no_red_pen = util.mask_rgb(rgb, mask_no_red_pen)
1049 | save_display(save, display, info, rgb_no_red_pen, slide_num, 4, "No Red Pen", "rgb-no-red-pen")
1050 |
1051 | mask_no_green_pen = filter_green_pen(rgb)
1052 | rgb_no_green_pen = util.mask_rgb(rgb, mask_no_green_pen)
1053 | save_display(save, display, info, rgb_no_green_pen, slide_num, 5, "No Green Pen", "rgb-no-green-pen")
1054 |
1055 | mask_no_blue_pen = filter_blue_pen(rgb)
1056 | rgb_no_blue_pen = util.mask_rgb(rgb, mask_no_blue_pen)
1057 | save_display(save, display, info, rgb_no_blue_pen, slide_num, 6, "No Blue Pen", "rgb-no-blue-pen")
1058 |
1059 | mask_gray_green_pens = mask_not_gray & mask_not_green & mask_no_red_pen & mask_no_green_pen & mask_no_blue_pen
1060 | rgb_gray_green_pens = util.mask_rgb(rgb, mask_gray_green_pens)
1061 | save_display(save, display, info, rgb_gray_green_pens, slide_num, 7, "Not Gray, Not Green, No Pens",
1062 | "rgb-no-gray-no-green-no-pens")
1063 |
1064 | mask_remove_small = filter_remove_small_objects(mask_gray_green_pens, min_size=500, output_type="bool")
1065 | rgb_remove_small = util.mask_rgb(rgb, mask_remove_small)
1066 | save_display(save, display, info, rgb_remove_small, slide_num, 8,
1067 | "Not Gray, Not Green, No Pens,\nRemove Small Objects",
1068 | "rgb-not-green-not-gray-no-pens-remove-small")
1069 |
1070 | img = rgb_remove_small
1071 | return img
1072 |
1073 |
1074 | def apply_filters_to_image(slide_num, save=True, display=False):
1075 | """
1076 | Apply a set of filters to an image and optionally save and/or display filtered images.
1077 |
1078 | Args:
1079 | slide_num: The slide number.
1080 | save: If True, save filtered images.
1081 | display: If True, display filtered images to screen.
1082 |
1083 | Returns:
1084 | Tuple consisting of 1) the resulting filtered image as a NumPy array, and 2) dictionary of image information
1085 | (used for HTML page generation).
1086 | """
1087 | t = Time()
1088 | print("Processing slide #%d" % slide_num)
1089 |
1090 | info = dict()
1091 |
1092 | if save and not os.path.exists(slide.FILTER_DIR):
1093 | os.makedirs(slide.FILTER_DIR)
1094 | img_path = slide.get_training_image_path(slide_num)
1095 | np_orig = slide.open_image_np(img_path)
1096 | filtered_np_img = apply_image_filters(np_orig, slide_num, info, save=save, display=display)
1097 |
1098 | if save:
1099 | t1 = Time()
1100 | result_path = slide.get_filter_image_result(slide_num)
1101 | pil_img = util.np_to_pil(filtered_np_img)
1102 | pil_img.save(result_path)
1103 | print("%-20s | Time: %-14s Name: %s" % ("Save Image", str(t1.elapsed()), result_path))
1104 |
1105 | t1 = Time()
1106 | thumbnail_path = slide.get_filter_thumbnail_result(slide_num)
1107 | slide.save_thumbnail(pil_img, slide.THUMBNAIL_SIZE, thumbnail_path)
1108 | print("%-20s | Time: %-14s Name: %s" % ("Save Thumbnail", str(t1.elapsed()), thumbnail_path))
1109 |
1110 | print("Slide #%03d processing time: %s\n" % (slide_num, str(t.elapsed())))
1111 |
1112 | return filtered_np_img, info
1113 |
1114 |
1115 | def save_display(save, display, info, np_img, slide_num, filter_num, display_text, file_text,
1116 | display_mask_percentage=True):
1117 | """
1118 | Optionally save an image and/or display the image.
1119 |
1120 | Args:
1121 | save: If True, save filtered images.
1122 | display: If True, display filtered images to screen.
1123 | info: Dictionary to store filter information.
1124 | np_img: Image as a NumPy array.
1125 | slide_num: The slide number.
1126 | filter_num: The filter number.
1127 | display_text: Filter display name.
1128 | file_text: Filter name for file.
1129 | display_mask_percentage: If True, display mask percentage on displayed slide.
1130 | """
1131 | mask_percentage = None
1132 | if display_mask_percentage:
1133 | mask_percentage = mask_percent(np_img)
1134 | display_text = display_text + "\n(" + mask_percentage_text(mask_percentage) + " masked)"
1135 | if slide_num is None and filter_num is None:
1136 | pass
1137 | elif filter_num is None:
1138 | display_text = "S%03d " % slide_num + display_text
1139 | elif slide_num is None:
1140 | display_text = "F%03d " % filter_num + display_text
1141 | else:
1142 | display_text = "S%03d-F%03d " % (slide_num, filter_num) + display_text
1143 | if display:
1144 | util.display_img(np_img, display_text)
1145 | if save:
1146 | save_filtered_image(np_img, slide_num, filter_num, file_text)
1147 | if info is not None:
1148 | info[slide_num * 1000 + filter_num] = (slide_num, filter_num, display_text, file_text, mask_percentage)
1149 |
1150 |
1151 | def mask_percentage_text(mask_percentage):
1152 | """
1153 | Generate a formatted string representing the percentage that an image is masked.
1154 |
1155 | Args:
1156 | mask_percentage: The mask percentage.
1157 |
1158 | Returns:
1159 | The mask percentage formatted as a string.
1160 | """
1161 | return "%3.2f%%" % mask_percentage
1162 |
1163 |
1164 | def image_cell(slide_num, filter_num, display_text, file_text):
1165 | """
1166 | Generate HTML for viewing a processed image.
1167 |
1168 | Args:
1169 | slide_num: The slide number.
1170 | filter_num: The filter number.
1171 | display_text: Filter display name.
1172 | file_text: Filter name for file.
1173 |
1174 | Returns:
1175 | HTML for a table cell for viewing a filtered image.
1176 | """
1177 | filt_img = slide.get_filter_image_path(slide_num, filter_num, file_text)
1178 | filt_thumb = slide.get_filter_thumbnail_path(slide_num, filter_num, file_text)
1179 | img_name = slide.get_filter_image_filename(slide_num, filter_num, file_text)
1180 | return "
\n" + \
1181 | " %s \n" % (filt_img, display_text) + \
1182 | " \n" % (filt_thumb) + \
1183 | " \n" + \
1184 | " | \n"
1185 |
1186 |
1187 | def html_header(page_title):
1188 | """
1189 | Generate an HTML header for previewing images.
1190 |
1191 | Returns:
1192 | HTML header for viewing images.
1193 | """
1194 | html = "\n" + \
1196 | "\n" + \
1197 | " \n" + \
1198 | " %s\n" % page_title + \
1199 | " \n" + \
1203 | " \n" + \
1204 | " \n"
1205 | return html
1206 |
1207 |
1208 | def html_footer():
1209 | """
1210 | Generate an HTML footer for previewing images.
1211 |
1212 | Returns:
1213 | HTML footer for viewing images.
1214 | """
1215 | html = "\n" + \
1216 | "\n"
1217 | return html
1218 |
1219 |
1220 | def save_filtered_image(np_img, slide_num, filter_num, filter_text):
1221 | """
1222 | Save a filtered image to the file system.
1223 |
1224 | Args:
1225 | np_img: Image as a NumPy array.
1226 | slide_num: The slide number.
1227 | filter_num: The filter number.
1228 | filter_text: Descriptive text to add to the image filename.
1229 | """
1230 | t = Time()
1231 | filepath = slide.get_filter_image_path(slide_num, filter_num, filter_text)
1232 | pil_img = util.np_to_pil(np_img)
1233 | pil_img.save(filepath)
1234 | print("%-20s | Time: %-14s Name: %s" % ("Save Image", str(t.elapsed()), filepath))
1235 |
1236 | t1 = Time()
1237 | thumbnail_filepath = slide.get_filter_thumbnail_path(slide_num, filter_num, filter_text)
1238 | slide.save_thumbnail(pil_img, slide.THUMBNAIL_SIZE, thumbnail_filepath)
1239 | print("%-20s | Time: %-14s Name: %s" % ("Save Thumbnail", str(t1.elapsed()), thumbnail_filepath))
1240 |
1241 |
1242 | def generate_filter_html_result(html_page_info):
1243 | """
1244 | Generate HTML to view the filtered images. If slide.FILTER_PAGINATE is True, the results will be paginated.
1245 |
1246 | Args:
1247 | html_page_info: Dictionary of image information.
1248 | """
1249 | if not slide.FILTER_PAGINATE:
1250 | html = ""
1251 | html += html_header("Filtered Images")
1252 | html += " \n"
1253 |
1254 | row = 0
1255 | for key in sorted(html_page_info):
1256 | value = html_page_info[key]
1257 | current_row = value[0]
1258 | if current_row > row:
1259 | html += " \n"
1260 | row = current_row
1261 | html += image_cell(value[0], value[1], value[2], value[3])
1262 | next_key = key + 1
1263 | if next_key not in html_page_info:
1264 | html += "
\n"
1265 |
1266 | html += "
\n"
1267 | html += html_footer()
1268 | text_file = open(os.path.join(slide.FILTER_HTML_DIR, "filters.html"), "w")
1269 | text_file.write(html)
1270 | text_file.close()
1271 | else:
1272 | slide_nums = set()
1273 | for key in html_page_info:
1274 | slide_num = math.floor(key / 1000)
1275 | slide_nums.add(slide_num)
1276 | slide_nums = sorted(list(slide_nums))
1277 | total_len = len(slide_nums)
1278 | page_size = slide.FILTER_PAGINATION_SIZE
1279 | num_pages = math.ceil(total_len / page_size)
1280 |
1281 | for page_num in range(1, num_pages + 1):
1282 | start_index = (page_num - 1) * page_size
1283 | end_index = (page_num * page_size) if (page_num < num_pages) else total_len
1284 | page_slide_nums = slide_nums[start_index:end_index]
1285 |
1286 | html = ""
1287 | html += html_header("Filtered Images, Page %d" % page_num)
1288 |
1289 | html += " "
1290 | if page_num > 1:
1291 | if page_num == 2:
1292 | html += "
< "
1293 | else:
1294 | html += "
< " % (page_num - 1)
1295 | html += "Page %d" % page_num
1296 | if page_num < num_pages:
1297 | html += "
> " % (page_num + 1)
1298 | html += "
\n"
1299 |
1300 | html += " \n"
1301 | for slide_num in page_slide_nums:
1302 | html += " \n"
1303 | filter_num = 1
1304 |
1305 | lookup_key = slide_num * 1000 + filter_num
1306 | while lookup_key in html_page_info:
1307 | value = html_page_info[lookup_key]
1308 | html += image_cell(value[0], value[1], value[2], value[3])
1309 | lookup_key += 1
1310 | html += "
\n"
1311 |
1312 | html += "
\n"
1313 |
1314 | html += html_footer()
1315 | if page_num == 1:
1316 | text_file = open(os.path.join(slide.FILTER_HTML_DIR, "filters.html"), "w")
1317 | else:
1318 | text_file = open(os.path.join(slide.FILTER_HTML_DIR, "filters-%d.html" % page_num), "w")
1319 | text_file.write(html)
1320 | text_file.close()
1321 |
1322 |
1323 | def apply_filters_to_image_list(image_num_list, save, display):
1324 | """
1325 | Apply filters to a list of images.
1326 |
1327 | Args:
1328 | image_num_list: List of image numbers.
1329 | save: If True, save filtered images.
1330 | display: If True, display filtered images to screen.
1331 |
1332 | Returns:
1333 | Tuple consisting of 1) a list of image numbers, and 2) a dictionary of image filter information.
1334 | """
1335 | html_page_info = dict()
1336 | for slide_num in image_num_list:
1337 | _, info = apply_filters_to_image(slide_num, save=save, display=display)
1338 | html_page_info.update(info)
1339 | return image_num_list, html_page_info
1340 |
1341 |
1342 | def apply_filters_to_image_range(start_ind, end_ind, save, display):
1343 | """
1344 | Apply filters to a range of images.
1345 |
1346 | Args:
1347 | start_ind: Starting index (inclusive).
1348 | end_ind: Ending index (inclusive).
1349 | save: If True, save filtered images.
1350 | display: If True, display filtered images to screen.
1351 |
1352 | Returns:
1353 | Tuple consisting of 1) staring index of slides converted to images, 2) ending index of slides converted to images,
1354 | and 3) a dictionary of image filter information.
1355 | """
1356 | html_page_info = dict()
1357 | for slide_num in range(start_ind, end_ind + 1):
1358 | _, info = apply_filters_to_image(slide_num, save=save, display=display)
1359 | html_page_info.update(info)
1360 | return start_ind, end_ind, html_page_info
1361 |
1362 |
1363 | def singleprocess_apply_filters_to_images(save=True, display=False, html=True, image_num_list=None):
1364 | """
1365 | Apply a set of filters to training images and optionally save and/or display the filtered images.
1366 |
1367 | Args:
1368 | save: If True, save filtered images.
1369 | display: If True, display filtered images to screen.
1370 | html: If True, generate HTML page to display filtered images.
1371 | image_num_list: Optionally specify a list of image slide numbers.
1372 | """
1373 | t = Time()
1374 | print("Applying filters to images\n")
1375 |
1376 | if image_num_list is not None:
1377 | _, info = apply_filters_to_image_list(image_num_list, save, display)
1378 | else:
1379 | num_training_slides = slide.get_num_training_slides()
1380 | (s, e, info) = apply_filters_to_image_range(1, num_training_slides, save, display)
1381 |
1382 | print("Time to apply filters to all images: %s\n" % str(t.elapsed()))
1383 |
1384 | if html:
1385 | generate_filter_html_result(info)
1386 |
1387 |
1388 | def multiprocess_apply_filters_to_images(save=True, display=False, html=True, image_num_list=None):
1389 | """
1390 | Apply a set of filters to all training images using multiple processes (one process per core).
1391 |
1392 | Args:
1393 | save: If True, save filtered images.
1394 | display: If True, display filtered images to screen (multiprocessed display not recommended).
1395 | html: If True, generate HTML page to display filtered images.
1396 | image_num_list: Optionally specify a list of image slide numbers.
1397 | """
1398 | timer = Time()
1399 | print("Applying filters to images (multiprocess)\n")
1400 |
1401 | if save and not os.path.exists(slide.FILTER_DIR):
1402 | os.makedirs(slide.FILTER_DIR)
1403 |
1404 | # how many processes to use
1405 | num_processes = multiprocessing.cpu_count()
1406 | pool = multiprocessing.Pool(num_processes)
1407 |
1408 | if image_num_list is not None:
1409 | num_train_images = len(image_num_list)
1410 | else:
1411 | num_train_images = slide.get_num_training_slides()
1412 | if num_processes > num_train_images:
1413 | num_processes = num_train_images
1414 | images_per_process = num_train_images / num_processes
1415 |
1416 | print("Number of processes: " + str(num_processes))
1417 | print("Number of training images: " + str(num_train_images))
1418 |
1419 | tasks = []
1420 | for num_process in range(1, num_processes + 1):
1421 | start_index = (num_process - 1) * images_per_process + 1
1422 | end_index = num_process * images_per_process
1423 | start_index = int(start_index)
1424 | end_index = int(end_index)
1425 | if image_num_list is not None:
1426 | sublist = image_num_list[start_index - 1:end_index]
1427 | tasks.append((sublist, save, display))
1428 | print("Task #" + str(num_process) + ": Process slides " + str(sublist))
1429 | else:
1430 | tasks.append((start_index, end_index, save, display))
1431 | if start_index == end_index:
1432 | print("Task #" + str(num_process) + ": Process slide " + str(start_index))
1433 | else:
1434 | print("Task #" + str(num_process) + ": Process slides " + str(start_index) + " to " + str(end_index))
1435 |
1436 | # start tasks
1437 | results = []
1438 | for t in tasks:
1439 | if image_num_list is not None:
1440 | results.append(pool.apply_async(apply_filters_to_image_list, t))
1441 | else:
1442 | results.append(pool.apply_async(apply_filters_to_image_range, t))
1443 |
1444 | html_page_info = dict()
1445 | for result in results:
1446 | if image_num_list is not None:
1447 | (image_nums, html_page_info_res) = result.get()
1448 | html_page_info.update(html_page_info_res)
1449 | print("Done filtering slides: %s" % image_nums)
1450 | else:
1451 | (start_ind, end_ind, html_page_info_res) = result.get()
1452 | html_page_info.update(html_page_info_res)
1453 | if (start_ind == end_ind):
1454 | print("Done filtering slide %d" % start_ind)
1455 | else:
1456 | print("Done filtering slides %d through %d" % (start_ind, end_ind))
1457 |
1458 | if html:
1459 | generate_filter_html_result(html_page_info)
1460 |
1461 | print("Time to apply filters to all images (multiprocess): %s\n" % str(timer.elapsed()))
1462 |
1463 | # if __name__ == "__main__":
1464 | # slide.training_slide_to_image(2)
1465 | # singleprocess_apply_filters_to_images(image_num_list=[2], display=True)
1466 |
1467 | # singleprocess_apply_filters_to_images()
1468 | # multiprocess_apply_filters_to_images()
1469 |
--------------------------------------------------------------------------------
/deephistopath/wsi/slide.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | #
15 | # ------------------------------------------------------------------------
16 |
17 | import glob
18 | import math
19 | import matplotlib.pyplot as plt
20 | import multiprocessing
21 | import numpy as np
22 | import openslide
23 | from openslide import OpenSlideError
24 | import os
25 | import PIL
26 | from PIL import Image
27 | import re
28 | import sys
29 | from deephistopath.wsi import util
30 | from deephistopath.wsi.util import Time
31 |
32 | BASE_DIR = os.path.join(".", "data")
33 | # BASE_DIR = os.path.join(os.sep, "Volumes", "BigData", "TUPAC")
34 | TRAIN_PREFIX = "TUPAC-TR-"
35 | SRC_TRAIN_DIR = os.path.join(BASE_DIR, "training_slides")
36 | SRC_TRAIN_EXT = "svs"
37 | DEST_TRAIN_SUFFIX = "" # Example: "train-"
38 | DEST_TRAIN_EXT = "png"
39 | SCALE_FACTOR = 32
40 | DEST_TRAIN_DIR = os.path.join(BASE_DIR, "training_" + DEST_TRAIN_EXT)
41 | THUMBNAIL_SIZE = 300
42 | THUMBNAIL_EXT = "jpg"
43 |
44 | DEST_TRAIN_THUMBNAIL_DIR = os.path.join(BASE_DIR, "training_thumbnail_" + THUMBNAIL_EXT)
45 |
46 | FILTER_SUFFIX = "" # Example: "filter-"
47 | FILTER_RESULT_TEXT = "filtered"
48 | FILTER_DIR = os.path.join(BASE_DIR, "filter_" + DEST_TRAIN_EXT)
49 | FILTER_THUMBNAIL_DIR = os.path.join(BASE_DIR, "filter_thumbnail_" + THUMBNAIL_EXT)
50 | FILTER_PAGINATION_SIZE = 50
51 | FILTER_PAGINATE = True
52 | FILTER_HTML_DIR = BASE_DIR
53 |
54 | TILE_SUMMARY_DIR = os.path.join(BASE_DIR, "tile_summary_" + DEST_TRAIN_EXT)
55 | TILE_SUMMARY_ON_ORIGINAL_DIR = os.path.join(BASE_DIR, "tile_summary_on_original_" + DEST_TRAIN_EXT)
56 | TILE_SUMMARY_SUFFIX = "tile_summary"
57 | TILE_SUMMARY_THUMBNAIL_DIR = os.path.join(BASE_DIR, "tile_summary_thumbnail_" + THUMBNAIL_EXT)
58 | TILE_SUMMARY_ON_ORIGINAL_THUMBNAIL_DIR = os.path.join(BASE_DIR, "tile_summary_on_original_thumbnail_" + THUMBNAIL_EXT)
59 | TILE_SUMMARY_PAGINATION_SIZE = 50
60 | TILE_SUMMARY_PAGINATE = True
61 | TILE_SUMMARY_HTML_DIR = BASE_DIR
62 |
63 | TILE_DATA_DIR = os.path.join(BASE_DIR, "tile_data")
64 | TILE_DATA_SUFFIX = "tile_data"
65 |
66 | TOP_TILES_SUFFIX = "top_tile_summary"
67 | TOP_TILES_DIR = os.path.join(BASE_DIR, TOP_TILES_SUFFIX + "_" + DEST_TRAIN_EXT)
68 | TOP_TILES_THUMBNAIL_DIR = os.path.join(BASE_DIR, TOP_TILES_SUFFIX + "_thumbnail_" + THUMBNAIL_EXT)
69 | TOP_TILES_ON_ORIGINAL_DIR = os.path.join(BASE_DIR, TOP_TILES_SUFFIX + "_on_original_" + DEST_TRAIN_EXT)
70 | TOP_TILES_ON_ORIGINAL_THUMBNAIL_DIR = os.path.join(BASE_DIR,
71 | TOP_TILES_SUFFIX + "_on_original_thumbnail_" + THUMBNAIL_EXT)
72 |
73 | TILE_DIR = os.path.join(BASE_DIR, "tiles_" + DEST_TRAIN_EXT)
74 | TILE_SUFFIX = "tile"
75 |
76 | STATS_DIR = os.path.join(BASE_DIR, "svs_stats")
77 |
78 |
79 | def open_slide(filename):
80 | """
81 | Open a whole-slide image (*.svs, etc).
82 |
83 | Args:
84 | filename: Name of the slide file.
85 |
86 | Returns:
87 | An OpenSlide object representing a whole-slide image.
88 | """
89 | try:
90 | slide = openslide.open_slide(filename)
91 | except OpenSlideError:
92 | slide = None
93 | except FileNotFoundError:
94 | slide = None
95 | return slide
96 |
97 |
98 | def open_image(filename):
99 | """
100 | Open an image (*.jpg, *.png, etc).
101 |
102 | Args:
103 | filename: Name of the image file.
104 |
105 | returns:
106 | A PIL.Image.Image object representing an image.
107 | """
108 | image = Image.open(filename)
109 | return image
110 |
111 |
112 | def open_image_np(filename):
113 | """
114 | Open an image (*.jpg, *.png, etc) as an RGB NumPy array.
115 |
116 | Args:
117 | filename: Name of the image file.
118 |
119 | returns:
120 | A NumPy representing an RGB image.
121 | """
122 | pil_img = open_image(filename)
123 | np_img = util.pil_to_np_rgb(pil_img)
124 | return np_img
125 |
126 |
127 | def get_training_slide_path(slide_number):
128 | """
129 | Convert slide number to a path to the corresponding WSI training slide file.
130 |
131 | Example:
132 | 5 -> ../data/training_slides/TUPAC-TR-005.svs
133 |
134 | Args:
135 | slide_number: The slide number.
136 |
137 | Returns:
138 | Path to the WSI training slide file.
139 | """
140 | padded_sl_num = str(slide_number).zfill(3)
141 | slide_filepath = os.path.join(SRC_TRAIN_DIR, TRAIN_PREFIX + padded_sl_num + "." + SRC_TRAIN_EXT)
142 | return slide_filepath
143 |
144 |
145 | def get_tile_image_path(tile):
146 | """
147 | Obtain tile image path based on tile information such as row, column, row pixel position, column pixel position,
148 | pixel width, and pixel height.
149 |
150 | Args:
151 | tile: Tile object.
152 |
153 | Returns:
154 | Path to image tile.
155 | """
156 | t = tile
157 | padded_sl_num = str(t.slide_num).zfill(3)
158 | tile_path = os.path.join(TILE_DIR, padded_sl_num,
159 | TRAIN_PREFIX + padded_sl_num + "-" + TILE_SUFFIX + "-r%d-c%d-x%d-y%d-w%d-h%d" % (
160 | t.r, t.c, t.o_c_s, t.o_r_s, t.o_c_e - t.o_c_s, t.o_r_e - t.o_r_s) + "." + DEST_TRAIN_EXT)
161 | return tile_path
162 |
163 |
164 | def get_tile_image_path_by_slide_row_col(slide_number, row, col):
165 | """
166 | Obtain tile image path using wildcard lookup with slide number, row, and column.
167 |
168 | Args:
169 | slide_number: The slide number.
170 | row: The row.
171 | col: The column.
172 |
173 | Returns:
174 | Path to image tile.
175 | """
176 | padded_sl_num = str(slide_number).zfill(3)
177 | wilcard_path = os.path.join(TILE_DIR, padded_sl_num,
178 | TRAIN_PREFIX + padded_sl_num + "-" + TILE_SUFFIX + "-r%d-c%d-*." % (
179 | row, col) + DEST_TRAIN_EXT)
180 | img_path = glob.glob(wilcard_path)[0]
181 | return img_path
182 |
183 |
184 | def get_training_image_path(slide_number, large_w=None, large_h=None, small_w=None, small_h=None):
185 | """
186 | Convert slide number and optional dimensions to a training image path. If no dimensions are supplied,
187 | the corresponding file based on the slide number will be looked up in the file system using a wildcard.
188 |
189 | Example:
190 | 5 -> ../data/training_png/TUPAC-TR-005-32x-49920x108288-1560x3384.png
191 |
192 | Args:
193 | slide_number: The slide number.
194 | large_w: Large image width.
195 | large_h: Large image height.
196 | small_w: Small image width.
197 | small_h: Small image height.
198 |
199 | Returns:
200 | Path to the image file.
201 | """
202 | padded_sl_num = str(slide_number).zfill(3)
203 | if large_w is None and large_h is None and small_w is None and small_h is None:
204 | wildcard_path = os.path.join(DEST_TRAIN_DIR, TRAIN_PREFIX + padded_sl_num + "*." + DEST_TRAIN_EXT)
205 | img_path = glob.glob(wildcard_path)[0]
206 | else:
207 | img_path = os.path.join(DEST_TRAIN_DIR, TRAIN_PREFIX + padded_sl_num + "-" + str(
208 | SCALE_FACTOR) + "x-" + DEST_TRAIN_SUFFIX + str(
209 | large_w) + "x" + str(large_h) + "-" + str(small_w) + "x" + str(small_h) + "." + DEST_TRAIN_EXT)
210 | return img_path
211 |
212 |
213 | def get_training_thumbnail_path(slide_number, large_w=None, large_h=None, small_w=None, small_h=None):
214 | """
215 | Convert slide number and optional dimensions to a training thumbnail path. If no dimensions are
216 | supplied, the corresponding file based on the slide number will be looked up in the file system using a wildcard.
217 |
218 | Example:
219 | 5 -> ../data/training_thumbnail_jpg/TUPAC-TR-005-32x-49920x108288-1560x3384.jpg
220 |
221 | Args:
222 | slide_number: The slide number.
223 | large_w: Large image width.
224 | large_h: Large image height.
225 | small_w: Small image width.
226 | small_h: Small image height.
227 |
228 | Returns:
229 | Path to the thumbnail file.
230 | """
231 | padded_sl_num = str(slide_number).zfill(3)
232 | if large_w is None and large_h is None and small_w is None and small_h is None:
233 | wilcard_path = os.path.join(DEST_TRAIN_THUMBNAIL_DIR, TRAIN_PREFIX + padded_sl_num + "*." + THUMBNAIL_EXT)
234 | img_path = glob.glob(wilcard_path)[0]
235 | else:
236 | img_path = os.path.join(DEST_TRAIN_THUMBNAIL_DIR, TRAIN_PREFIX + padded_sl_num + "-" + str(
237 | SCALE_FACTOR) + "x-" + DEST_TRAIN_SUFFIX + str(
238 | large_w) + "x" + str(large_h) + "-" + str(small_w) + "x" + str(small_h) + "." + THUMBNAIL_EXT)
239 | return img_path
240 |
241 |
242 | def get_filter_image_path(slide_number, filter_number, filter_name_info):
243 | """
244 | Convert slide number, filter number, and text to a path to a filter image file.
245 |
246 | Example:
247 | 5, 1, "rgb" -> ../data/filter_png/TUPAC-TR-005-001-rgb.png
248 |
249 | Args:
250 | slide_number: The slide number.
251 | filter_number: The filter number.
252 | filter_name_info: Descriptive text describing filter.
253 |
254 | Returns:
255 | Path to the filter image file.
256 | """
257 | dir = FILTER_DIR
258 | if not os.path.exists(dir):
259 | os.makedirs(dir)
260 | img_path = os.path.join(dir, get_filter_image_filename(slide_number, filter_number, filter_name_info))
261 | return img_path
262 |
263 |
264 | def get_filter_thumbnail_path(slide_number, filter_number, filter_name_info):
265 | """
266 | Convert slide number, filter number, and text to a path to a filter thumbnail file.
267 |
268 | Example:
269 | 5, 1, "rgb" -> ../data/filter_thumbnail_jpg/TUPAC-TR-005-001-rgb.jpg
270 |
271 | Args:
272 | slide_number: The slide number.
273 | filter_number: The filter number.
274 | filter_name_info: Descriptive text describing filter.
275 |
276 | Returns:
277 | Path to the filter thumbnail file.
278 | """
279 | dir = FILTER_THUMBNAIL_DIR
280 | if not os.path.exists(dir):
281 | os.makedirs(dir)
282 | img_path = os.path.join(dir, get_filter_image_filename(slide_number, filter_number, filter_name_info, thumbnail=True))
283 | return img_path
284 |
285 |
286 | def get_filter_image_filename(slide_number, filter_number, filter_name_info, thumbnail=False):
287 | """
288 | Convert slide number, filter number, and text to a filter file name.
289 |
290 | Example:
291 | 5, 1, "rgb", False -> TUPAC-TR-005-001-rgb.png
292 | 5, 1, "rgb", True -> TUPAC-TR-005-001-rgb.jpg
293 |
294 | Args:
295 | slide_number: The slide number.
296 | filter_number: The filter number.
297 | filter_name_info: Descriptive text describing filter.
298 | thumbnail: If True, produce thumbnail filename.
299 |
300 | Returns:
301 | The filter image or thumbnail file name.
302 | """
303 | if thumbnail:
304 | ext = THUMBNAIL_EXT
305 | else:
306 | ext = DEST_TRAIN_EXT
307 | padded_sl_num = str(slide_number).zfill(3)
308 | padded_fi_num = str(filter_number).zfill(3)
309 | img_filename = TRAIN_PREFIX + padded_sl_num + "-" + padded_fi_num + "-" + FILTER_SUFFIX + filter_name_info + "." + ext
310 | return img_filename
311 |
312 |
313 | def get_tile_summary_image_path(slide_number):
314 | """
315 | Convert slide number to a path to a tile summary image file.
316 |
317 | Example:
318 | 5 -> ../data/tile_summary_png/TUPAC-TR-005-tile_summary.png
319 |
320 | Args:
321 | slide_number: The slide number.
322 |
323 | Returns:
324 | Path to the tile summary image file.
325 | """
326 | if not os.path.exists(TILE_SUMMARY_DIR):
327 | os.makedirs(TILE_SUMMARY_DIR)
328 | img_path = os.path.join(TILE_SUMMARY_DIR, get_tile_summary_image_filename(slide_number))
329 | return img_path
330 |
331 |
332 | def get_tile_summary_thumbnail_path(slide_number):
333 | """
334 | Convert slide number to a path to a tile summary thumbnail file.
335 |
336 | Example:
337 | 5 -> ../data/tile_summary_thumbnail_jpg/TUPAC-TR-005-tile_summary.jpg
338 |
339 | Args:
340 | slide_number: The slide number.
341 |
342 | Returns:
343 | Path to the tile summary thumbnail file.
344 | """
345 | if not os.path.exists(TILE_SUMMARY_THUMBNAIL_DIR):
346 | os.makedirs(TILE_SUMMARY_THUMBNAIL_DIR)
347 | img_path = os.path.join(TILE_SUMMARY_THUMBNAIL_DIR, get_tile_summary_image_filename(slide_number, thumbnail=True))
348 | return img_path
349 |
350 |
351 | def get_tile_summary_on_original_image_path(slide_number):
352 | """
353 | Convert slide number to a path to a tile summary on original image file.
354 |
355 | Example:
356 | 5 -> ../data/tile_summary_on_original_png/TUPAC-TR-005-tile_summary.png
357 |
358 | Args:
359 | slide_number: The slide number.
360 |
361 | Returns:
362 | Path to the tile summary on original image file.
363 | """
364 | if not os.path.exists(TILE_SUMMARY_ON_ORIGINAL_DIR):
365 | os.makedirs(TILE_SUMMARY_ON_ORIGINAL_DIR)
366 | img_path = os.path.join(TILE_SUMMARY_ON_ORIGINAL_DIR, get_tile_summary_image_filename(slide_number))
367 | return img_path
368 |
369 |
370 | def get_tile_summary_on_original_thumbnail_path(slide_number):
371 | """
372 | Convert slide number to a path to a tile summary on original thumbnail file.
373 |
374 | Example:
375 | 5 -> ../data/tile_summary_on_original_thumbnail_jpg/TUPAC-TR-005-tile_summary.jpg
376 |
377 | Args:
378 | slide_number: The slide number.
379 |
380 | Returns:
381 | Path to the tile summary on original thumbnail file.
382 | """
383 | if not os.path.exists(TILE_SUMMARY_ON_ORIGINAL_THUMBNAIL_DIR):
384 | os.makedirs(TILE_SUMMARY_ON_ORIGINAL_THUMBNAIL_DIR)
385 | img_path = os.path.join(TILE_SUMMARY_ON_ORIGINAL_THUMBNAIL_DIR,
386 | get_tile_summary_image_filename(slide_number, thumbnail=True))
387 | return img_path
388 |
389 |
390 | def get_top_tiles_on_original_image_path(slide_number):
391 | """
392 | Convert slide number to a path to a top tiles on original image file.
393 |
394 | Example:
395 | 5 -> ../data/top_tiles_on_original_png/TUPAC-TR-005-32x-49920x108288-1560x3384-top_tiles.png
396 |
397 | Args:
398 | slide_number: The slide number.
399 |
400 | Returns:
401 | Path to the top tiles on original image file.
402 | """
403 | if not os.path.exists(TOP_TILES_ON_ORIGINAL_DIR):
404 | os.makedirs(TOP_TILES_ON_ORIGINAL_DIR)
405 | img_path = os.path.join(TOP_TILES_ON_ORIGINAL_DIR, get_top_tiles_image_filename(slide_number))
406 | return img_path
407 |
408 |
409 | def get_top_tiles_on_original_thumbnail_path(slide_number):
410 | """
411 | Convert slide number to a path to a top tiles on original thumbnail file.
412 |
413 | Example:
414 | 5 -> ../data/top_tiles_on_original_thumbnail_jpg/TUPAC-TR-005-32x-49920x108288-1560x3384-top_tiles.jpg
415 |
416 | Args:
417 | slide_number: The slide number.
418 |
419 | Returns:
420 | Path to the top tiles on original thumbnail file.
421 | """
422 | if not os.path.exists(TOP_TILES_ON_ORIGINAL_THUMBNAIL_DIR):
423 | os.makedirs(TOP_TILES_ON_ORIGINAL_THUMBNAIL_DIR)
424 | img_path = os.path.join(TOP_TILES_ON_ORIGINAL_THUMBNAIL_DIR,
425 | get_top_tiles_image_filename(slide_number, thumbnail=True))
426 | return img_path
427 |
428 |
429 | def get_tile_summary_image_filename(slide_number, thumbnail=False):
430 | """
431 | Convert slide number to a tile summary image file name.
432 |
433 | Example:
434 | 5, False -> TUPAC-TR-005-tile_summary.png
435 | 5, True -> TUPAC-TR-005-tile_summary.jpg
436 |
437 | Args:
438 | slide_number: The slide number.
439 | thumbnail: If True, produce thumbnail filename.
440 |
441 | Returns:
442 | The tile summary image file name.
443 | """
444 | if thumbnail:
445 | ext = THUMBNAIL_EXT
446 | else:
447 | ext = DEST_TRAIN_EXT
448 | padded_sl_num = str(slide_number).zfill(3)
449 |
450 | training_img_path = get_training_image_path(slide_number)
451 | large_w, large_h, small_w, small_h = parse_dimensions_from_image_filename(training_img_path)
452 | img_filename = TRAIN_PREFIX + padded_sl_num + "-" + str(SCALE_FACTOR) + "x-" + str(large_w) + "x" + str(
453 | large_h) + "-" + str(small_w) + "x" + str(small_h) + "-" + TILE_SUMMARY_SUFFIX + "." + ext
454 |
455 | return img_filename
456 |
457 |
458 | def get_top_tiles_image_filename(slide_number, thumbnail=False):
459 | """
460 | Convert slide number to a top tiles image file name.
461 |
462 | Example:
463 | 5, False -> TUPAC-TR-005-32x-49920x108288-1560x3384-top_tiles.png
464 | 5, True -> TUPAC-TR-005-32x-49920x108288-1560x3384-top_tiles.jpg
465 |
466 | Args:
467 | slide_number: The slide number.
468 | thumbnail: If True, produce thumbnail filename.
469 |
470 | Returns:
471 | The top tiles image file name.
472 | """
473 | if thumbnail:
474 | ext = THUMBNAIL_EXT
475 | else:
476 | ext = DEST_TRAIN_EXT
477 | padded_sl_num = str(slide_number).zfill(3)
478 |
479 | training_img_path = get_training_image_path(slide_number)
480 | large_w, large_h, small_w, small_h = parse_dimensions_from_image_filename(training_img_path)
481 | img_filename = TRAIN_PREFIX + padded_sl_num + "-" + str(SCALE_FACTOR) + "x-" + str(large_w) + "x" + str(
482 | large_h) + "-" + str(small_w) + "x" + str(small_h) + "-" + TOP_TILES_SUFFIX + "." + ext
483 |
484 | return img_filename
485 |
486 |
487 | def get_top_tiles_image_path(slide_number):
488 | """
489 | Convert slide number to a path to a top tiles image file.
490 |
491 | Example:
492 | 5 -> ../data/top_tiles_png/TUPAC-TR-005-32x-49920x108288-1560x3384-top_tiles.png
493 |
494 | Args:
495 | slide_number: The slide number.
496 |
497 | Returns:
498 | Path to the top tiles image file.
499 | """
500 | if not os.path.exists(TOP_TILES_DIR):
501 | os.makedirs(TOP_TILES_DIR)
502 | img_path = os.path.join(TOP_TILES_DIR, get_top_tiles_image_filename(slide_number))
503 | return img_path
504 |
505 |
506 | def get_top_tiles_thumbnail_path(slide_number):
507 | """
508 | Convert slide number to a path to a tile summary thumbnail file.
509 |
510 | Example:
511 | 5 -> ../data/top_tiles_thumbnail_jpg/TUPAC-TR-005-32x-49920x108288-1560x3384-top_tiles.jpg
512 | Args:
513 | slide_number: The slide number.
514 |
515 | Returns:
516 | Path to the top tiles thumbnail file.
517 | """
518 | if not os.path.exists(TOP_TILES_THUMBNAIL_DIR):
519 | os.makedirs(TOP_TILES_THUMBNAIL_DIR)
520 | img_path = os.path.join(TOP_TILES_THUMBNAIL_DIR, get_top_tiles_image_filename(slide_number, thumbnail=True))
521 | return img_path
522 |
523 |
524 | def get_tile_data_filename(slide_number):
525 | """
526 | Convert slide number to a tile data file name.
527 |
528 | Example:
529 | 5 -> TUPAC-TR-005-32x-49920x108288-1560x3384-tile_data.csv
530 |
531 | Args:
532 | slide_number: The slide number.
533 |
534 | Returns:
535 | The tile data file name.
536 | """
537 | padded_sl_num = str(slide_number).zfill(3)
538 |
539 | training_img_path = get_training_image_path(slide_number)
540 | large_w, large_h, small_w, small_h = parse_dimensions_from_image_filename(training_img_path)
541 | data_filename = TRAIN_PREFIX + padded_sl_num + "-" + str(SCALE_FACTOR) + "x-" + str(large_w) + "x" + str(
542 | large_h) + "-" + str(small_w) + "x" + str(small_h) + "-" + TILE_DATA_SUFFIX + ".csv"
543 |
544 | return data_filename
545 |
546 |
547 | def get_tile_data_path(slide_number):
548 | """
549 | Convert slide number to a path to a tile data file.
550 |
551 | Example:
552 | 5 -> ../data/tile_data/TUPAC-TR-005-32x-49920x108288-1560x3384-tile_data.csv
553 |
554 | Args:
555 | slide_number: The slide number.
556 |
557 | Returns:
558 | Path to the tile data file.
559 | """
560 | if not os.path.exists(TILE_DATA_DIR):
561 | os.makedirs(TILE_DATA_DIR)
562 | file_path = os.path.join(TILE_DATA_DIR, get_tile_data_filename(slide_number))
563 | return file_path
564 |
565 |
566 | def get_filter_image_result(slide_number):
567 | """
568 | Convert slide number to the path to the file that is the final result of filtering.
569 |
570 | Example:
571 | 5 -> ../data/filter_png/TUPAC-TR-005-32x-49920x108288-1560x3384-filtered.png
572 |
573 | Args:
574 | slide_number: The slide number.
575 |
576 | Returns:
577 | Path to the filter image file.
578 | """
579 | padded_sl_num = str(slide_number).zfill(3)
580 | training_img_path = get_training_image_path(slide_number)
581 | large_w, large_h, small_w, small_h = parse_dimensions_from_image_filename(training_img_path)
582 | img_path = os.path.join(FILTER_DIR, TRAIN_PREFIX + padded_sl_num + "-" + str(
583 | SCALE_FACTOR) + "x-" + FILTER_SUFFIX + str(large_w) + "x" + str(large_h) + "-" + str(small_w) + "x" + str(
584 | small_h) + "-" + FILTER_RESULT_TEXT + "." + DEST_TRAIN_EXT)
585 | return img_path
586 |
587 |
588 | def get_filter_thumbnail_result(slide_number):
589 | """
590 | Convert slide number to the path to the file that is the final thumbnail result of filtering.
591 |
592 | Example:
593 | 5 -> ../data/filter_thumbnail_jpg/TUPAC-TR-005-32x-49920x108288-1560x3384-filtered.jpg
594 |
595 | Args:
596 | slide_number: The slide number.
597 |
598 | Returns:
599 | Path to the filter thumbnail file.
600 | """
601 | padded_sl_num = str(slide_number).zfill(3)
602 | training_img_path = get_training_image_path(slide_number)
603 | large_w, large_h, small_w, small_h = parse_dimensions_from_image_filename(training_img_path)
604 | img_path = os.path.join(FILTER_THUMBNAIL_DIR, TRAIN_PREFIX + padded_sl_num + "-" + str(
605 | SCALE_FACTOR) + "x-" + FILTER_SUFFIX + str(large_w) + "x" + str(large_h) + "-" + str(small_w) + "x" + str(
606 | small_h) + "-" + FILTER_RESULT_TEXT + "." + THUMBNAIL_EXT)
607 | return img_path
608 |
609 |
610 | def parse_dimensions_from_image_filename(filename):
611 | """
612 | Parse an image filename to extract the original width and height and the converted width and height.
613 |
614 | Example:
615 | "TUPAC-TR-011-32x-97103x79079-3034x2471-tile_summary.png" -> (97103, 79079, 3034, 2471)
616 |
617 | Args:
618 | filename: The image filename.
619 |
620 | Returns:
621 | Tuple consisting of the original width, original height, the converted width, and the converted height.
622 | """
623 | m = re.match(".*-([\d]*)x([\d]*)-([\d]*)x([\d]*).*\..*", filename)
624 | large_w = int(m.group(1))
625 | large_h = int(m.group(2))
626 | small_w = int(m.group(3))
627 | small_h = int(m.group(4))
628 | return large_w, large_h, small_w, small_h
629 |
630 |
631 | def small_to_large_mapping(small_pixel, large_dimensions):
632 | """
633 | Map a scaled-down pixel width and height to the corresponding pixel of the original whole-slide image.
634 |
635 | Args:
636 | small_pixel: The scaled-down width and height.
637 | large_dimensions: The width and height of the original whole-slide image.
638 |
639 | Returns:
640 | Tuple consisting of the scaled-up width and height.
641 | """
642 | small_x, small_y = small_pixel
643 | large_w, large_h = large_dimensions
644 | large_x = round((large_w / SCALE_FACTOR) / math.floor(large_w / SCALE_FACTOR) * (SCALE_FACTOR * small_x))
645 | large_y = round((large_h / SCALE_FACTOR) / math.floor(large_h / SCALE_FACTOR) * (SCALE_FACTOR * small_y))
646 | return large_x, large_y
647 |
648 |
649 | def training_slide_to_image(slide_number):
650 | """
651 | Convert a WSI training slide to a saved scaled-down image in a format such as jpg or png.
652 |
653 | Args:
654 | slide_number: The slide number.
655 | """
656 |
657 | img, large_w, large_h, new_w, new_h = slide_to_scaled_pil_image(slide_number)
658 |
659 | img_path = get_training_image_path(slide_number, large_w, large_h, new_w, new_h)
660 | print("Saving image to: " + img_path)
661 | if not os.path.exists(DEST_TRAIN_DIR):
662 | os.makedirs(DEST_TRAIN_DIR)
663 | img.save(img_path)
664 |
665 | thumbnail_path = get_training_thumbnail_path(slide_number, large_w, large_h, new_w, new_h)
666 | save_thumbnail(img, THUMBNAIL_SIZE, thumbnail_path)
667 |
668 |
669 | def slide_to_scaled_pil_image(slide_number):
670 | """
671 | Convert a WSI training slide to a scaled-down PIL image.
672 |
673 | Args:
674 | slide_number: The slide number.
675 |
676 | Returns:
677 | Tuple consisting of scaled-down PIL image, original width, original height, new width, and new height.
678 | """
679 | slide_filepath = get_training_slide_path(slide_number)
680 | print("Opening Slide #%d: %s" % (slide_number, slide_filepath))
681 | slide = open_slide(slide_filepath)
682 |
683 | large_w, large_h = slide.dimensions
684 | new_w = math.floor(large_w / SCALE_FACTOR)
685 | new_h = math.floor(large_h / SCALE_FACTOR)
686 | level = slide.get_best_level_for_downsample(SCALE_FACTOR)
687 | whole_slide_image = slide.read_region((0, 0), level, slide.level_dimensions[level])
688 | whole_slide_image = whole_slide_image.convert("RGB")
689 | img = whole_slide_image.resize((new_w, new_h), PIL.Image.BILINEAR)
690 | return img, large_w, large_h, new_w, new_h
691 |
692 |
693 | def slide_to_scaled_np_image(slide_number):
694 | """
695 | Convert a WSI training slide to a scaled-down NumPy image.
696 |
697 | Args:
698 | slide_number: The slide number.
699 |
700 | Returns:
701 | Tuple consisting of scaled-down NumPy image, original width, original height, new width, and new height.
702 | """
703 | pil_img, large_w, large_h, new_w, new_h = slide_to_scaled_pil_image(slide_number)
704 | np_img = util.pil_to_np_rgb(pil_img)
705 | return np_img, large_w, large_h, new_w, new_h
706 |
707 |
708 | def show_slide(slide_number):
709 | """
710 | Display a WSI slide on the screen, where the slide has been scaled down and converted to a PIL image.
711 |
712 | Args:
713 | slide_number: The slide number.
714 | """
715 | pil_img = slide_to_scaled_pil_image(slide_number)[0]
716 | pil_img.show()
717 |
718 |
719 | def save_thumbnail(pil_img, size, path, display_path=False):
720 | """
721 | Save a thumbnail of a PIL image, specifying the maximum width or height of the thumbnail.
722 |
723 | Args:
724 | pil_img: The PIL image to save as a thumbnail.
725 | size: The maximum width or height of the thumbnail.
726 | path: The path to the thumbnail.
727 | display_path: If True, display thumbnail path in console.
728 | """
729 | max_size = tuple(round(size * d / max(pil_img.size)) for d in pil_img.size)
730 | img = pil_img.resize(max_size, PIL.Image.BILINEAR)
731 | if display_path:
732 | print("Saving thumbnail to: " + path)
733 | dir = os.path.dirname(path)
734 | if dir != '' and not os.path.exists(dir):
735 | os.makedirs(dir)
736 | img.save(path)
737 |
738 |
739 | def get_num_training_slides():
740 | """
741 | Obtain the total number of WSI training slide images.
742 |
743 | Returns:
744 | The total number of WSI training slide images.
745 | """
746 | num_training_slides = len(glob.glob1(SRC_TRAIN_DIR, "*." + SRC_TRAIN_EXT))
747 | return num_training_slides
748 |
749 |
750 | def training_slide_range_to_images(start_ind, end_ind):
751 | """
752 | Convert a range of WSI training slides to smaller images (in a format such as jpg or png).
753 |
754 | Args:
755 | start_ind: Starting index (inclusive).
756 | end_ind: Ending index (inclusive).
757 |
758 | Returns:
759 | The starting index and the ending index of the slides that were converted.
760 | """
761 | for slide_num in range(start_ind, end_ind + 1):
762 | training_slide_to_image(slide_num)
763 | return (start_ind, end_ind)
764 |
765 |
766 | def singleprocess_training_slides_to_images():
767 | """
768 | Convert all WSI training slides to smaller images using a single process.
769 | """
770 | t = Time()
771 |
772 | num_train_images = get_num_training_slides()
773 | training_slide_range_to_images(1, num_train_images)
774 |
775 | t.elapsed_display()
776 |
777 |
778 | def multiprocess_training_slides_to_images():
779 | """
780 | Convert all WSI training slides to smaller images using multiple processes (one process per core).
781 | Each process will process a range of slide numbers.
782 | """
783 | timer = Time()
784 |
785 | # how many processes to use
786 | num_processes = multiprocessing.cpu_count()
787 | pool = multiprocessing.Pool(num_processes)
788 |
789 | num_train_images = get_num_training_slides()
790 | if num_processes > num_train_images:
791 | num_processes = num_train_images
792 | images_per_process = num_train_images / num_processes
793 |
794 | print("Number of processes: " + str(num_processes))
795 | print("Number of training images: " + str(num_train_images))
796 |
797 | # each task specifies a range of slides
798 | tasks = []
799 | for num_process in range(1, num_processes + 1):
800 | start_index = (num_process - 1) * images_per_process + 1
801 | end_index = num_process * images_per_process
802 | start_index = int(start_index)
803 | end_index = int(end_index)
804 | tasks.append((start_index, end_index))
805 | if start_index == end_index:
806 | print("Task #" + str(num_process) + ": Process slide " + str(start_index))
807 | else:
808 | print("Task #" + str(num_process) + ": Process slides " + str(start_index) + " to " + str(end_index))
809 |
810 | # start tasks
811 | results = []
812 | for t in tasks:
813 | results.append(pool.apply_async(training_slide_range_to_images, t))
814 |
815 | for result in results:
816 | (start_ind, end_ind) = result.get()
817 | if start_ind == end_ind:
818 | print("Done converting slide %d" % start_ind)
819 | else:
820 | print("Done converting slides %d through %d" % (start_ind, end_ind))
821 |
822 | timer.elapsed_display()
823 |
824 |
825 | def slide_stats():
826 | """
827 | Display statistics/graphs about training slides.
828 | """
829 | t = Time()
830 |
831 | if not os.path.exists(STATS_DIR):
832 | os.makedirs(STATS_DIR)
833 |
834 | num_train_images = get_num_training_slides()
835 | slide_stats = []
836 | for slide_num in range(1, num_train_images + 1):
837 | slide_filepath = get_training_slide_path(slide_num)
838 | print("Opening Slide #%d: %s" % (slide_num, slide_filepath))
839 | slide = open_slide(slide_filepath)
840 | (width, height) = slide.dimensions
841 | print(" Dimensions: {:,d} x {:,d}".format(width, height))
842 | slide_stats.append((width, height))
843 |
844 | max_width = 0
845 | max_height = 0
846 | min_width = sys.maxsize
847 | min_height = sys.maxsize
848 | total_width = 0
849 | total_height = 0
850 | total_size = 0
851 | which_max_width = 0
852 | which_max_height = 0
853 | which_min_width = 0
854 | which_min_height = 0
855 | max_size = 0
856 | min_size = sys.maxsize
857 | which_max_size = 0
858 | which_min_size = 0
859 | for z in range(0, num_train_images):
860 | (width, height) = slide_stats[z]
861 | if width > max_width:
862 | max_width = width
863 | which_max_width = z + 1
864 | if width < min_width:
865 | min_width = width
866 | which_min_width = z + 1
867 | if height > max_height:
868 | max_height = height
869 | which_max_height = z + 1
870 | if height < min_height:
871 | min_height = height
872 | which_min_height = z + 1
873 | size = width * height
874 | if size > max_size:
875 | max_size = size
876 | which_max_size = z + 1
877 | if size < min_size:
878 | min_size = size
879 | which_min_size = z + 1
880 | total_width = total_width + width
881 | total_height = total_height + height
882 | total_size = total_size + size
883 |
884 | avg_width = total_width / num_train_images
885 | avg_height = total_height / num_train_images
886 | avg_size = total_size / num_train_images
887 |
888 | stats_string = ""
889 | stats_string += "%-11s {:14,d} pixels (slide #%d)".format(max_width) % ("Max width:", which_max_width)
890 | stats_string += "\n%-11s {:14,d} pixels (slide #%d)".format(max_height) % ("Max height:", which_max_height)
891 | stats_string += "\n%-11s {:14,d} pixels (slide #%d)".format(max_size) % ("Max size:", which_max_size)
892 | stats_string += "\n%-11s {:14,d} pixels (slide #%d)".format(min_width) % ("Min width:", which_min_width)
893 | stats_string += "\n%-11s {:14,d} pixels (slide #%d)".format(min_height) % ("Min height:", which_min_height)
894 | stats_string += "\n%-11s {:14,d} pixels (slide #%d)".format(min_size) % ("Min size:", which_min_size)
895 | stats_string += "\n%-11s {:14,d} pixels".format(round(avg_width)) % "Avg width:"
896 | stats_string += "\n%-11s {:14,d} pixels".format(round(avg_height)) % "Avg height:"
897 | stats_string += "\n%-11s {:14,d} pixels".format(round(avg_size)) % "Avg size:"
898 | stats_string += "\n"
899 | print(stats_string)
900 |
901 | stats_string += "\nslide number,width,height"
902 | for i in range(0, len(slide_stats)):
903 | (width, height) = slide_stats[i]
904 | stats_string += "\n%d,%d,%d" % (i + 1, width, height)
905 | stats_string += "\n"
906 |
907 | stats_file = open(os.path.join(STATS_DIR, "stats.txt"), "w")
908 | stats_file.write(stats_string)
909 | stats_file.close()
910 |
911 | t.elapsed_display()
912 |
913 | x, y = zip(*slide_stats)
914 | colors = np.random.rand(num_train_images)
915 | sizes = [10 for n in range(num_train_images)]
916 | plt.scatter(x, y, s=sizes, c=colors, alpha=0.7)
917 | plt.xlabel("width (pixels)")
918 | plt.ylabel("height (pixels)")
919 | plt.title("SVS Image Sizes")
920 | plt.set_cmap("prism")
921 | plt.tight_layout()
922 | plt.savefig(os.path.join(STATS_DIR, "svs-image-sizes.png"))
923 | plt.show()
924 |
925 | plt.clf()
926 | plt.scatter(x, y, s=sizes, c=colors, alpha=0.7)
927 | plt.xlabel("width (pixels)")
928 | plt.ylabel("height (pixels)")
929 | plt.title("SVS Image Sizes (Labeled with slide numbers)")
930 | plt.set_cmap("prism")
931 | for i in range(num_train_images):
932 | snum = i + 1
933 | plt.annotate(str(snum), (x[i], y[i]))
934 | plt.tight_layout()
935 | plt.savefig(os.path.join(STATS_DIR, "svs-image-sizes-slide-numbers.png"))
936 | plt.show()
937 |
938 | plt.clf()
939 | area = [w * h / 1000000 for (w, h) in slide_stats]
940 | plt.hist(area, bins=64)
941 | plt.xlabel("width x height (M of pixels)")
942 | plt.ylabel("# images")
943 | plt.title("Distribution of image sizes in millions of pixels")
944 | plt.tight_layout()
945 | plt.savefig(os.path.join(STATS_DIR, "distribution-of-svs-image-sizes.png"))
946 | plt.show()
947 |
948 | plt.clf()
949 | whratio = [w / h for (w, h) in slide_stats]
950 | plt.hist(whratio, bins=64)
951 | plt.xlabel("width to height ratio")
952 | plt.ylabel("# images")
953 | plt.title("Image shapes (width to height)")
954 | plt.tight_layout()
955 | plt.savefig(os.path.join(STATS_DIR, "w-to-h.png"))
956 | plt.show()
957 |
958 | plt.clf()
959 | hwratio = [h / w for (w, h) in slide_stats]
960 | plt.hist(hwratio, bins=64)
961 | plt.xlabel("height to width ratio")
962 | plt.ylabel("# images")
963 | plt.title("Image shapes (height to width)")
964 | plt.tight_layout()
965 | plt.savefig(os.path.join(STATS_DIR, "h-to-w.png"))
966 | plt.show()
967 |
968 |
969 | def slide_info(display_all_properties=False):
970 | """
971 | Display information (such as properties) about training images.
972 |
973 | Args:
974 | display_all_properties: If True, display all available slide properties.
975 | """
976 | t = Time()
977 |
978 | num_train_images = get_num_training_slides()
979 | obj_pow_20_list = []
980 | obj_pow_40_list = []
981 | obj_pow_other_list = []
982 | for slide_num in range(1, num_train_images + 1):
983 | slide_filepath = get_training_slide_path(slide_num)
984 | print("\nOpening Slide #%d: %s" % (slide_num, slide_filepath))
985 | slide = open_slide(slide_filepath)
986 | print("Level count: %d" % slide.level_count)
987 | print("Level dimensions: " + str(slide.level_dimensions))
988 | print("Level downsamples: " + str(slide.level_downsamples))
989 | print("Dimensions: " + str(slide.dimensions))
990 | objective_power = int(slide.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER])
991 | print("Objective power: " + str(objective_power))
992 | if objective_power == 20:
993 | obj_pow_20_list.append(slide_num)
994 | elif objective_power == 40:
995 | obj_pow_40_list.append(slide_num)
996 | else:
997 | obj_pow_other_list.append(slide_num)
998 | print("Associated images:")
999 | for ai_key in slide.associated_images.keys():
1000 | print(" " + str(ai_key) + ": " + str(slide.associated_images.get(ai_key)))
1001 | print("Format: " + str(slide.detect_format(slide_filepath)))
1002 | if display_all_properties:
1003 | print("Properties:")
1004 | for prop_key in slide.properties.keys():
1005 | print(" Property: " + str(prop_key) + ", value: " + str(slide.properties.get(prop_key)))
1006 |
1007 | print("\n\nSlide Magnifications:")
1008 | print(" 20x Slides: " + str(obj_pow_20_list))
1009 | print(" 40x Slides: " + str(obj_pow_40_list))
1010 | print(" ??x Slides: " + str(obj_pow_other_list) + "\n")
1011 |
1012 | t.elapsed_display()
1013 |
1014 |
1015 | # if __name__ == "__main__":
1016 | # show_slide(2)
1017 | # slide_info(display_all_properties=True)
1018 | # slide_stats()
1019 |
1020 | # training_slide_to_image(4)
1021 | # img_path = get_training_image_path(4)
1022 | # img = open_image(img_path)
1023 | # img.show()
1024 |
1025 | # slide_to_scaled_pil_image(5)[0].show()
1026 | # singleprocess_training_slides_to_images()
1027 | # multiprocess_training_slides_to_images()
1028 |
--------------------------------------------------------------------------------
/deephistopath/wsi/tiles.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | #
15 | # ------------------------------------------------------------------------
16 |
17 | # To get around renderer issue on macOS going from Matplotlib image to NumPy image.
18 | import matplotlib
19 |
20 | matplotlib.use('Agg')
21 |
22 | import colorsys
23 | import math
24 | import matplotlib.pyplot as plt
25 | import multiprocessing
26 | import numpy as np
27 | import os
28 | from PIL import Image, ImageDraw, ImageFont
29 | from enum import Enum
30 | from deephistopath.wsi import util
31 | from deephistopath.wsi import filter
32 | from deephistopath.wsi import slide
33 | from deephistopath.wsi.util import Time
34 |
35 | TISSUE_HIGH_THRESH = 80
36 | TISSUE_LOW_THRESH = 10
37 |
38 | ROW_TILE_SIZE = 1024
39 | COL_TILE_SIZE = 1024
40 | NUM_TOP_TILES = 50
41 |
42 | DISPLAY_TILE_SUMMARY_LABELS = False
43 | TILE_LABEL_TEXT_SIZE = 10
44 | LABEL_ALL_TILES_IN_TOP_TILE_SUMMARY = False
45 | BORDER_ALL_TILES_IN_TOP_TILE_SUMMARY = False
46 |
47 | TILE_BORDER_SIZE = 2 # The size of the colored rectangular border around summary tiles.
48 |
49 | HIGH_COLOR = (0, 255, 0)
50 | MEDIUM_COLOR = (255, 255, 0)
51 | LOW_COLOR = (255, 165, 0)
52 | NONE_COLOR = (255, 0, 0)
53 |
54 | FADED_THRESH_COLOR = (128, 255, 128)
55 | FADED_MEDIUM_COLOR = (255, 255, 128)
56 | FADED_LOW_COLOR = (255, 210, 128)
57 | FADED_NONE_COLOR = (255, 128, 128)
58 |
59 | FONT_PATH = "/Library/Fonts/Arial Bold.ttf"
60 | SUMMARY_TITLE_FONT_PATH = "/Library/Fonts/Courier New Bold.ttf"
61 | SUMMARY_TITLE_TEXT_COLOR = (0, 0, 0)
62 | SUMMARY_TITLE_TEXT_SIZE = 24
63 | SUMMARY_TILE_TEXT_COLOR = (255, 255, 255)
64 | TILE_TEXT_COLOR = (0, 0, 0)
65 | TILE_TEXT_SIZE = 36
66 | TILE_TEXT_BACKGROUND_COLOR = (255, 255, 255)
67 | TILE_TEXT_W_BORDER = 5
68 | TILE_TEXT_H_BORDER = 4
69 |
70 | HSV_PURPLE = 270
71 | HSV_PINK = 330
72 |
73 |
74 | def get_num_tiles(rows, cols, row_tile_size, col_tile_size):
75 | """
76 | Obtain the number of vertical and horizontal tiles that an image can be divided into given a row tile size and
77 | a column tile size.
78 |
79 | Args:
80 | rows: Number of rows.
81 | cols: Number of columns.
82 | row_tile_size: Number of pixels in a tile row.
83 | col_tile_size: Number of pixels in a tile column.
84 |
85 | Returns:
86 | Tuple consisting of the number of vertical tiles and the number of horizontal tiles that the image can be divided
87 | into given the row tile size and the column tile size.
88 | """
89 | num_row_tiles = math.ceil(rows / row_tile_size)
90 | num_col_tiles = math.ceil(cols / col_tile_size)
91 | return num_row_tiles, num_col_tiles
92 |
93 |
94 | def get_tile_indices(rows, cols, row_tile_size, col_tile_size):
95 | """
96 | Obtain a list of tile coordinates (starting row, ending row, starting column, ending column, row number, column number).
97 |
98 | Args:
99 | rows: Number of rows.
100 | cols: Number of columns.
101 | row_tile_size: Number of pixels in a tile row.
102 | col_tile_size: Number of pixels in a tile column.
103 |
104 | Returns:
105 | List of tuples representing tile coordinates consisting of starting row, ending row,
106 | starting column, ending column, row number, column number.
107 | """
108 | indices = list()
109 | num_row_tiles, num_col_tiles = get_num_tiles(rows, cols, row_tile_size, col_tile_size)
110 | for r in range(0, num_row_tiles):
111 | start_r = r * row_tile_size
112 | end_r = ((r + 1) * row_tile_size) if (r < num_row_tiles - 1) else rows
113 | for c in range(0, num_col_tiles):
114 | start_c = c * col_tile_size
115 | end_c = ((c + 1) * col_tile_size) if (c < num_col_tiles - 1) else cols
116 | indices.append((start_r, end_r, start_c, end_c, r + 1, c + 1))
117 | return indices
118 |
119 |
120 | def create_summary_pil_img(np_img, title_area_height, row_tile_size, col_tile_size, num_row_tiles, num_col_tiles):
121 | """
122 | Create a PIL summary image including top title area and right side and bottom padding.
123 |
124 | Args:
125 | np_img: Image as a NumPy array.
126 | title_area_height: Height of the title area at the top of the summary image.
127 | row_tile_size: The tile size in rows.
128 | col_tile_size: The tile size in columns.
129 | num_row_tiles: The number of row tiles.
130 | num_col_tiles: The number of column tiles.
131 |
132 | Returns:
133 | Summary image as a PIL image. This image contains the image data specified by the np_img input and also has
134 | potentially a top title area and right side and bottom padding.
135 | """
136 | r = row_tile_size * num_row_tiles + title_area_height
137 | c = col_tile_size * num_col_tiles
138 | summary_img = np.zeros([r, c, np_img.shape[2]], dtype=np.uint8)
139 | # add gray edges so that tile text does not get cut off
140 | summary_img.fill(120)
141 | # color title area white
142 | summary_img[0:title_area_height, 0:summary_img.shape[1]].fill(255)
143 | summary_img[title_area_height:np_img.shape[0] + title_area_height, 0:np_img.shape[1]] = np_img
144 | summary = util.np_to_pil(summary_img)
145 | return summary
146 |
147 |
148 | def generate_tile_summaries(tile_sum, np_img, display=True, save_summary=False):
149 | """
150 | Generate summary images/thumbnails showing a 'heatmap' representation of the tissue segmentation of all tiles.
151 |
152 | Args:
153 | tile_sum: TileSummary object.
154 | np_img: Image as a NumPy array.
155 | display: If True, display tile summary to screen.
156 | save_summary: If True, save tile summary images.
157 | """
158 | z = 300 # height of area at top of summary slide
159 | slide_num = tile_sum.slide_num
160 | rows = tile_sum.scaled_h
161 | cols = tile_sum.scaled_w
162 | row_tile_size = tile_sum.scaled_tile_h
163 | col_tile_size = tile_sum.scaled_tile_w
164 | num_row_tiles, num_col_tiles = get_num_tiles(rows, cols, row_tile_size, col_tile_size)
165 | summary = create_summary_pil_img(np_img, z, row_tile_size, col_tile_size, num_row_tiles, num_col_tiles)
166 | draw = ImageDraw.Draw(summary)
167 |
168 | original_img_path = slide.get_training_image_path(slide_num)
169 | np_orig = slide.open_image_np(original_img_path)
170 | summary_orig = create_summary_pil_img(np_orig, z, row_tile_size, col_tile_size, num_row_tiles, num_col_tiles)
171 | draw_orig = ImageDraw.Draw(summary_orig)
172 |
173 | for t in tile_sum.tiles:
174 | border_color = tile_border_color(t.tissue_percentage)
175 | tile_border(draw, t.r_s + z, t.r_e + z, t.c_s, t.c_e, border_color)
176 | tile_border(draw_orig, t.r_s + z, t.r_e + z, t.c_s, t.c_e, border_color)
177 |
178 | summary_txt = summary_title(tile_sum) + "\n" + summary_stats(tile_sum)
179 |
180 | summary_font = ImageFont.truetype(SUMMARY_TITLE_FONT_PATH, size=SUMMARY_TITLE_TEXT_SIZE)
181 | draw.text((5, 5), summary_txt, SUMMARY_TITLE_TEXT_COLOR, font=summary_font)
182 | draw_orig.text((5, 5), summary_txt, SUMMARY_TITLE_TEXT_COLOR, font=summary_font)
183 |
184 | if DISPLAY_TILE_SUMMARY_LABELS:
185 | count = 0
186 | for t in tile_sum.tiles:
187 | count += 1
188 | label = "R%d\nC%d" % (t.r, t.c)
189 | font = ImageFont.truetype(FONT_PATH, size=TILE_LABEL_TEXT_SIZE)
190 | # drop shadow behind text
191 | draw.text(((t.c_s + 3), (t.r_s + 3 + z)), label, (0, 0, 0), font=font)
192 | draw_orig.text(((t.c_s + 3), (t.r_s + 3 + z)), label, (0, 0, 0), font=font)
193 |
194 | draw.text(((t.c_s + 2), (t.r_s + 2 + z)), label, SUMMARY_TILE_TEXT_COLOR, font=font)
195 | draw_orig.text(((t.c_s + 2), (t.r_s + 2 + z)), label, SUMMARY_TILE_TEXT_COLOR, font=font)
196 |
197 | if display:
198 | summary.show()
199 | summary_orig.show()
200 | if save_summary:
201 | save_tile_summary_image(summary, slide_num)
202 | save_tile_summary_on_original_image(summary_orig, slide_num)
203 |
204 |
205 | def generate_top_tile_summaries(tile_sum, np_img, display=True, save_summary=False, show_top_stats=True,
206 | label_all_tiles=LABEL_ALL_TILES_IN_TOP_TILE_SUMMARY,
207 | border_all_tiles=BORDER_ALL_TILES_IN_TOP_TILE_SUMMARY):
208 | """
209 | Generate summary images/thumbnails showing the top tiles ranked by score.
210 |
211 | Args:
212 | tile_sum: TileSummary object.
213 | np_img: Image as a NumPy array.
214 | display: If True, display top tiles to screen.
215 | save_summary: If True, save top tiles images.
216 | show_top_stats: If True, append top tile score stats to image.
217 | label_all_tiles: If True, label all tiles. If False, label only top tiles.
218 | """
219 | z = 300 # height of area at top of summary slide
220 | slide_num = tile_sum.slide_num
221 | rows = tile_sum.scaled_h
222 | cols = tile_sum.scaled_w
223 | row_tile_size = tile_sum.scaled_tile_h
224 | col_tile_size = tile_sum.scaled_tile_w
225 | num_row_tiles, num_col_tiles = get_num_tiles(rows, cols, row_tile_size, col_tile_size)
226 | summary = create_summary_pil_img(np_img, z, row_tile_size, col_tile_size, num_row_tiles, num_col_tiles)
227 | draw = ImageDraw.Draw(summary)
228 |
229 | original_img_path = slide.get_training_image_path(slide_num)
230 | np_orig = slide.open_image_np(original_img_path)
231 | summary_orig = create_summary_pil_img(np_orig, z, row_tile_size, col_tile_size, num_row_tiles, num_col_tiles)
232 | draw_orig = ImageDraw.Draw(summary_orig)
233 |
234 | if border_all_tiles:
235 | for t in tile_sum.tiles:
236 | border_color = faded_tile_border_color(t.tissue_percentage)
237 | tile_border(draw, t.r_s + z, t.r_e + z, t.c_s, t.c_e, border_color, border_size=1)
238 | tile_border(draw_orig, t.r_s + z, t.r_e + z, t.c_s, t.c_e, border_color, border_size=1)
239 |
240 | tbs = TILE_BORDER_SIZE
241 | top_tiles = tile_sum.top_tiles()
242 | for t in top_tiles:
243 | border_color = tile_border_color(t.tissue_percentage)
244 | tile_border(draw, t.r_s + z, t.r_e + z, t.c_s, t.c_e, border_color)
245 | tile_border(draw_orig, t.r_s + z, t.r_e + z, t.c_s, t.c_e, border_color)
246 | if border_all_tiles:
247 | tile_border(draw, t.r_s + z + tbs, t.r_e + z - tbs, t.c_s + tbs, t.c_e - tbs, (0, 0, 0))
248 | tile_border(draw_orig, t.r_s + z + tbs, t.r_e + z - tbs, t.c_s + tbs, t.c_e - tbs, (0, 0, 0))
249 |
250 | summary_title = "Slide %03d Top Tile Summary:" % slide_num
251 | summary_txt = summary_title + "\n" + summary_stats(tile_sum)
252 |
253 | summary_font = ImageFont.truetype(SUMMARY_TITLE_FONT_PATH, size=SUMMARY_TITLE_TEXT_SIZE)
254 | draw.text((5, 5), summary_txt, SUMMARY_TITLE_TEXT_COLOR, font=summary_font)
255 | draw_orig.text((5, 5), summary_txt, SUMMARY_TITLE_TEXT_COLOR, font=summary_font)
256 |
257 | tiles_to_label = tile_sum.tiles if label_all_tiles else top_tiles
258 | h_offset = TILE_BORDER_SIZE + 2
259 | v_offset = TILE_BORDER_SIZE
260 | h_ds_offset = TILE_BORDER_SIZE + 3
261 | v_ds_offset = TILE_BORDER_SIZE + 1
262 | for t in tiles_to_label:
263 | label = "R%d\nC%d" % (t.r, t.c)
264 | font = ImageFont.truetype(FONT_PATH, size=TILE_LABEL_TEXT_SIZE)
265 | # drop shadow behind text
266 | draw.text(((t.c_s + h_ds_offset), (t.r_s + v_ds_offset + z)), label, (0, 0, 0), font=font)
267 | draw_orig.text(((t.c_s + h_ds_offset), (t.r_s + v_ds_offset + z)), label, (0, 0, 0), font=font)
268 |
269 | draw.text(((t.c_s + h_offset), (t.r_s + v_offset + z)), label, SUMMARY_TILE_TEXT_COLOR, font=font)
270 | draw_orig.text(((t.c_s + h_offset), (t.r_s + v_offset + z)), label, SUMMARY_TILE_TEXT_COLOR, font=font)
271 |
272 | if show_top_stats:
273 | summary = add_tile_stats_to_top_tile_summary(summary, top_tiles, z)
274 | summary_orig = add_tile_stats_to_top_tile_summary(summary_orig, top_tiles, z)
275 |
276 | if display:
277 | summary.show()
278 | summary_orig.show()
279 | if save_summary:
280 | save_top_tiles_image(summary, slide_num)
281 | save_top_tiles_on_original_image(summary_orig, slide_num)
282 |
283 |
284 | def add_tile_stats_to_top_tile_summary(pil_img, tiles, z):
285 | np_sum = util.pil_to_np_rgb(pil_img)
286 | sum_r, sum_c, sum_ch = np_sum.shape
287 | np_stats = np_tile_stat_img(tiles)
288 | st_r, st_c, _ = np_stats.shape
289 | combo_c = sum_c + st_c
290 | combo_r = max(sum_r, st_r + z)
291 | combo = np.zeros([combo_r, combo_c, sum_ch], dtype=np.uint8)
292 | combo.fill(255)
293 | combo[0:sum_r, 0:sum_c] = np_sum
294 | combo[z:st_r + z, sum_c:sum_c + st_c] = np_stats
295 | result = util.np_to_pil(combo)
296 | return result
297 |
298 |
299 | def np_tile_stat_img(tiles):
300 | """
301 | Generate tile scoring statistics for a list of tiles and return the result as a NumPy array image.
302 |
303 | Args:
304 | tiles: List of tiles (such as top tiles)
305 |
306 | Returns:
307 | Tile scoring statistics converted into an NumPy array image.
308 | """
309 | tt = sorted(tiles, key=lambda t: (t.r, t.c), reverse=False)
310 | tile_stats = "Tile Score Statistics:\n"
311 | count = 0
312 | for t in tt:
313 | if count > 0:
314 | tile_stats += "\n"
315 | count += 1
316 | tup = (t.r, t.c, t.rank, t.tissue_percentage, t.color_factor, t.s_and_v_factor, t.quantity_factor, t.score)
317 | tile_stats += "R%03d C%03d #%003d TP:%6.2f%% CF:%4.0f SVF:%4.2f QF:%4.2f S:%0.4f" % tup
318 | np_stats = np_text(tile_stats, font_path=SUMMARY_TITLE_FONT_PATH, font_size=14)
319 | return np_stats
320 |
321 |
322 | def tile_border_color(tissue_percentage):
323 | """
324 | Obtain the corresponding tile border color for a particular tile tissue percentage.
325 |
326 | Args:
327 | tissue_percentage: The tile tissue percentage
328 |
329 | Returns:
330 | The tile border color corresponding to the tile tissue percentage.
331 | """
332 | if tissue_percentage >= TISSUE_HIGH_THRESH:
333 | border_color = HIGH_COLOR
334 | elif (tissue_percentage >= TISSUE_LOW_THRESH) and (tissue_percentage < TISSUE_HIGH_THRESH):
335 | border_color = MEDIUM_COLOR
336 | elif (tissue_percentage > 0) and (tissue_percentage < TISSUE_LOW_THRESH):
337 | border_color = LOW_COLOR
338 | else:
339 | border_color = NONE_COLOR
340 | return border_color
341 |
342 |
343 | def faded_tile_border_color(tissue_percentage):
344 | """
345 | Obtain the corresponding faded tile border color for a particular tile tissue percentage.
346 |
347 | Args:
348 | tissue_percentage: The tile tissue percentage
349 |
350 | Returns:
351 | The faded tile border color corresponding to the tile tissue percentage.
352 | """
353 | if tissue_percentage >= TISSUE_HIGH_THRESH:
354 | border_color = FADED_THRESH_COLOR
355 | elif (tissue_percentage >= TISSUE_LOW_THRESH) and (tissue_percentage < TISSUE_HIGH_THRESH):
356 | border_color = FADED_MEDIUM_COLOR
357 | elif (tissue_percentage > 0) and (tissue_percentage < TISSUE_LOW_THRESH):
358 | border_color = FADED_LOW_COLOR
359 | else:
360 | border_color = FADED_NONE_COLOR
361 | return border_color
362 |
363 |
364 | def summary_title(tile_summary):
365 | """
366 | Obtain tile summary title.
367 |
368 | Args:
369 | tile_summary: TileSummary object.
370 |
371 | Returns:
372 | The tile summary title.
373 | """
374 | return "Slide %03d Tile Summary:" % tile_summary.slide_num
375 |
376 |
377 | def summary_stats(tile_summary):
378 | """
379 | Obtain various stats about the slide tiles.
380 |
381 | Args:
382 | tile_summary: TileSummary object.
383 |
384 | Returns:
385 | Various stats about the slide tiles as a string.
386 | """
387 | return "Original Dimensions: %dx%d\n" % (tile_summary.orig_w, tile_summary.orig_h) + \
388 | "Original Tile Size: %dx%d\n" % (tile_summary.orig_tile_w, tile_summary.orig_tile_h) + \
389 | "Scale Factor: 1/%dx\n" % tile_summary.scale_factor + \
390 | "Scaled Dimensions: %dx%d\n" % (tile_summary.scaled_w, tile_summary.scaled_h) + \
391 | "Scaled Tile Size: %dx%d\n" % (tile_summary.scaled_tile_w, tile_summary.scaled_tile_w) + \
392 | "Total Mask: %3.2f%%, Total Tissue: %3.2f%%\n" % (
393 | tile_summary.mask_percentage(), tile_summary.tissue_percentage) + \
394 | "Tiles: %dx%d = %d\n" % (tile_summary.num_col_tiles, tile_summary.num_row_tiles, tile_summary.count) + \
395 | " %5d (%5.2f%%) tiles >=%d%% tissue\n" % (
396 | tile_summary.high, tile_summary.high / tile_summary.count * 100, TISSUE_HIGH_THRESH) + \
397 | " %5d (%5.2f%%) tiles >=%d%% and <%d%% tissue\n" % (
398 | tile_summary.medium, tile_summary.medium / tile_summary.count * 100, TISSUE_LOW_THRESH,
399 | TISSUE_HIGH_THRESH) + \
400 | " %5d (%5.2f%%) tiles >0%% and <%d%% tissue\n" % (
401 | tile_summary.low, tile_summary.low / tile_summary.count * 100, TISSUE_LOW_THRESH) + \
402 | " %5d (%5.2f%%) tiles =0%% tissue" % (tile_summary.none, tile_summary.none / tile_summary.count * 100)
403 |
404 |
405 | def tile_border(draw, r_s, r_e, c_s, c_e, color, border_size=TILE_BORDER_SIZE):
406 | """
407 | Draw a border around a tile with width TILE_BORDER_SIZE.
408 |
409 | Args:
410 | draw: Draw object for drawing on PIL image.
411 | r_s: Row starting pixel.
412 | r_e: Row ending pixel.
413 | c_s: Column starting pixel.
414 | c_e: Column ending pixel.
415 | color: Color of the border.
416 | border_size: Width of tile border in pixels.
417 | """
418 | for x in range(0, border_size):
419 | draw.rectangle([(c_s + x, r_s + x), (c_e - 1 - x, r_e - 1 - x)], outline=color)
420 |
421 |
422 | def save_tile_summary_image(pil_img, slide_num):
423 | """
424 | Save a tile summary image and thumbnail to the file system.
425 |
426 | Args:
427 | pil_img: Image as a PIL Image.
428 | slide_num: The slide number.
429 | """
430 | t = Time()
431 | filepath = slide.get_tile_summary_image_path(slide_num)
432 | pil_img.save(filepath)
433 | print("%-20s | Time: %-14s Name: %s" % ("Save Tile Sum", str(t.elapsed()), filepath))
434 |
435 | t = Time()
436 | thumbnail_filepath = slide.get_tile_summary_thumbnail_path(slide_num)
437 | slide.save_thumbnail(pil_img, slide.THUMBNAIL_SIZE, thumbnail_filepath)
438 | print("%-20s | Time: %-14s Name: %s" % ("Save Tile Sum Thumb", str(t.elapsed()), thumbnail_filepath))
439 |
440 |
441 | def save_top_tiles_image(pil_img, slide_num):
442 | """
443 | Save a top tiles image and thumbnail to the file system.
444 |
445 | Args:
446 | pil_img: Image as a PIL Image.
447 | slide_num: The slide number.
448 | """
449 | t = Time()
450 | filepath = slide.get_top_tiles_image_path(slide_num)
451 | pil_img.save(filepath)
452 | print("%-20s | Time: %-14s Name: %s" % ("Save Top Tiles Image", str(t.elapsed()), filepath))
453 |
454 | t = Time()
455 | thumbnail_filepath = slide.get_top_tiles_thumbnail_path(slide_num)
456 | slide.save_thumbnail(pil_img, slide.THUMBNAIL_SIZE, thumbnail_filepath)
457 | print("%-20s | Time: %-14s Name: %s" % ("Save Top Tiles Thumb", str(t.elapsed()), thumbnail_filepath))
458 |
459 |
460 | def save_tile_summary_on_original_image(pil_img, slide_num):
461 | """
462 | Save a tile summary on original image and thumbnail to the file system.
463 |
464 | Args:
465 | pil_img: Image as a PIL Image.
466 | slide_num: The slide number.
467 | """
468 | t = Time()
469 | filepath = slide.get_tile_summary_on_original_image_path(slide_num)
470 | pil_img.save(filepath)
471 | print("%-20s | Time: %-14s Name: %s" % ("Save Tile Sum Orig", str(t.elapsed()), filepath))
472 |
473 | t = Time()
474 | thumbnail_filepath = slide.get_tile_summary_on_original_thumbnail_path(slide_num)
475 | slide.save_thumbnail(pil_img, slide.THUMBNAIL_SIZE, thumbnail_filepath)
476 | print(
477 | "%-20s | Time: %-14s Name: %s" % ("Save Tile Sum Orig T", str(t.elapsed()), thumbnail_filepath))
478 |
479 |
480 | def save_top_tiles_on_original_image(pil_img, slide_num):
481 | """
482 | Save a top tiles on original image and thumbnail to the file system.
483 |
484 | Args:
485 | pil_img: Image as a PIL Image.
486 | slide_num: The slide number.
487 | """
488 | t = Time()
489 | filepath = slide.get_top_tiles_on_original_image_path(slide_num)
490 | pil_img.save(filepath)
491 | print("%-20s | Time: %-14s Name: %s" % ("Save Top Orig", str(t.elapsed()), filepath))
492 |
493 | t = Time()
494 | thumbnail_filepath = slide.get_top_tiles_on_original_thumbnail_path(slide_num)
495 | slide.save_thumbnail(pil_img, slide.THUMBNAIL_SIZE, thumbnail_filepath)
496 | print(
497 | "%-20s | Time: %-14s Name: %s" % ("Save Top Orig Thumb", str(t.elapsed()), thumbnail_filepath))
498 |
499 |
500 | def summary_and_tiles(slide_num, display=True, save_summary=False, save_data=True, save_top_tiles=True):
501 | """
502 | Generate tile summary and top tiles for slide.
503 |
504 | Args:
505 | slide_num: The slide number.
506 | display: If True, display tile summary to screen.
507 | save_summary: If True, save tile summary images.
508 | save_data: If True, save tile data to csv file.
509 | save_top_tiles: If True, save top tiles to files.
510 |
511 | """
512 | img_path = slide.get_filter_image_result(slide_num)
513 | np_img = slide.open_image_np(img_path)
514 |
515 | tile_sum = score_tiles(slide_num, np_img)
516 | if save_data:
517 | save_tile_data(tile_sum)
518 | generate_tile_summaries(tile_sum, np_img, display=display, save_summary=save_summary)
519 | generate_top_tile_summaries(tile_sum, np_img, display=display, save_summary=save_summary)
520 | if save_top_tiles:
521 | for tile in tile_sum.top_tiles():
522 | tile.save_tile()
523 | return tile_sum
524 |
525 |
526 | def save_tile_data(tile_summary):
527 | """
528 | Save tile data to csv file.
529 |
530 | Args
531 | tile_summary: TimeSummary object.
532 | """
533 |
534 | time = Time()
535 |
536 | csv = summary_title(tile_summary) + "\n" + summary_stats(tile_summary)
537 |
538 | csv += "\n\n\nTile Num,Row,Column,Tissue %,Tissue Quantity,Col Start,Row Start,Col End,Row End,Col Size,Row Size," + \
539 | "Original Col Start,Original Row Start,Original Col End,Original Row End,Original Col Size,Original Row Size," + \
540 | "Color Factor,S and V Factor,Quantity Factor,Score\n"
541 |
542 | for t in tile_summary.tiles:
543 | line = "%d,%d,%d,%4.2f,%s,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%4.0f,%4.2f,%4.2f,%0.4f\n" % (
544 | t.tile_num, t.r, t.c, t.tissue_percentage, t.tissue_quantity().name, t.c_s, t.r_s, t.c_e, t.r_e, t.c_e - t.c_s,
545 | t.r_e - t.r_s, t.o_c_s, t.o_r_s, t.o_c_e, t.o_r_e, t.o_c_e - t.o_c_s, t.o_r_e - t.o_r_s, t.color_factor,
546 | t.s_and_v_factor, t.quantity_factor, t.score)
547 | csv += line
548 |
549 | data_path = slide.get_tile_data_path(tile_summary.slide_num)
550 | csv_file = open(data_path, "w")
551 | csv_file.write(csv)
552 | csv_file.close()
553 |
554 | print("%-20s | Time: %-14s Name: %s" % ("Save Tile Data", str(time.elapsed()), data_path))
555 |
556 |
557 | def tile_to_pil_tile(tile):
558 | """
559 | Convert tile information into the corresponding tile as a PIL image read from the whole-slide image file.
560 |
561 | Args:
562 | tile: Tile object.
563 |
564 | Return:
565 | Tile as a PIL image.
566 | """
567 | t = tile
568 | slide_filepath = slide.get_training_slide_path(t.slide_num)
569 | s = slide.open_slide(slide_filepath)
570 |
571 | x, y = t.o_c_s, t.o_r_s
572 | w, h = t.o_c_e - t.o_c_s, t.o_r_e - t.o_r_s
573 | tile_region = s.read_region((x, y), 0, (w, h))
574 | # RGBA to RGB
575 | pil_img = tile_region.convert("RGB")
576 | return pil_img
577 |
578 |
579 | def tile_to_np_tile(tile):
580 | """
581 | Convert tile information into the corresponding tile as a NumPy image read from the whole-slide image file.
582 |
583 | Args:
584 | tile: Tile object.
585 |
586 | Return:
587 | Tile as a NumPy image.
588 | """
589 | pil_img = tile_to_pil_tile(tile)
590 | np_img = util.pil_to_np_rgb(pil_img)
591 | return np_img
592 |
593 |
594 | def save_display_tile(tile, save=True, display=False):
595 | """
596 | Save and/or display a tile image.
597 |
598 | Args:
599 | tile: Tile object.
600 | save: If True, save tile image.
601 | display: If True, dispaly tile image.
602 | """
603 | tile_pil_img = tile_to_pil_tile(tile)
604 |
605 | if save:
606 | t = Time()
607 | img_path = slide.get_tile_image_path(tile)
608 | dir = os.path.dirname(img_path)
609 | if not os.path.exists(dir):
610 | os.makedirs(dir)
611 | tile_pil_img.save(img_path)
612 | print("%-20s | Time: %-14s Name: %s" % ("Save Tile", str(t.elapsed()), img_path))
613 |
614 | if display:
615 | tile_pil_img.show()
616 |
617 |
618 | def score_tiles(slide_num, np_img=None, dimensions=None, small_tile_in_tile=False):
619 | """
620 | Score all tiles for a slide and return the results in a TileSummary object.
621 |
622 | Args:
623 | slide_num: The slide number.
624 | np_img: Optional image as a NumPy array.
625 | dimensions: Optional tuple consisting of (original width, original height, new width, new height). Used for dynamic
626 | tile retrieval.
627 | small_tile_in_tile: If True, include the small NumPy image in the Tile objects.
628 |
629 | Returns:
630 | TileSummary object which includes a list of Tile objects containing information about each tile.
631 | """
632 | if dimensions is None:
633 | img_path = slide.get_filter_image_result(slide_num)
634 | o_w, o_h, w, h = slide.parse_dimensions_from_image_filename(img_path)
635 | else:
636 | o_w, o_h, w, h = dimensions
637 |
638 | if np_img is None:
639 | np_img = slide.open_image_np(img_path)
640 |
641 | row_tile_size = round(ROW_TILE_SIZE / slide.SCALE_FACTOR) # use round?
642 | col_tile_size = round(COL_TILE_SIZE / slide.SCALE_FACTOR) # use round?
643 |
644 | num_row_tiles, num_col_tiles = get_num_tiles(h, w, row_tile_size, col_tile_size)
645 |
646 | tile_sum = TileSummary(slide_num=slide_num,
647 | orig_w=o_w,
648 | orig_h=o_h,
649 | orig_tile_w=COL_TILE_SIZE,
650 | orig_tile_h=ROW_TILE_SIZE,
651 | scaled_w=w,
652 | scaled_h=h,
653 | scaled_tile_w=col_tile_size,
654 | scaled_tile_h=row_tile_size,
655 | tissue_percentage=filter.tissue_percent(np_img),
656 | num_col_tiles=num_col_tiles,
657 | num_row_tiles=num_row_tiles)
658 |
659 | count = 0
660 | high = 0
661 | medium = 0
662 | low = 0
663 | none = 0
664 | tile_indices = get_tile_indices(h, w, row_tile_size, col_tile_size)
665 | for t in tile_indices:
666 | count += 1 # tile_num
667 | r_s, r_e, c_s, c_e, r, c = t
668 | np_tile = np_img[r_s:r_e, c_s:c_e]
669 | t_p = filter.tissue_percent(np_tile)
670 | amount = tissue_quantity(t_p)
671 | if amount == TissueQuantity.HIGH:
672 | high += 1
673 | elif amount == TissueQuantity.MEDIUM:
674 | medium += 1
675 | elif amount == TissueQuantity.LOW:
676 | low += 1
677 | elif amount == TissueQuantity.NONE:
678 | none += 1
679 | o_c_s, o_r_s = slide.small_to_large_mapping((c_s, r_s), (o_w, o_h))
680 | o_c_e, o_r_e = slide.small_to_large_mapping((c_e, r_e), (o_w, o_h))
681 |
682 | # pixel adjustment in case tile dimension too large (for example, 1025 instead of 1024)
683 | if (o_c_e - o_c_s) > COL_TILE_SIZE:
684 | o_c_e -= 1
685 | if (o_r_e - o_r_s) > ROW_TILE_SIZE:
686 | o_r_e -= 1
687 |
688 | score, color_factor, s_and_v_factor, quantity_factor = score_tile(np_tile, t_p, slide_num, r, c)
689 |
690 | np_scaled_tile = np_tile if small_tile_in_tile else None
691 | tile = Tile(tile_sum, slide_num, np_scaled_tile, count, r, c, r_s, r_e, c_s, c_e, o_r_s, o_r_e, o_c_s,
692 | o_c_e, t_p, color_factor, s_and_v_factor, quantity_factor, score)
693 | tile_sum.tiles.append(tile)
694 |
695 | tile_sum.count = count
696 | tile_sum.high = high
697 | tile_sum.medium = medium
698 | tile_sum.low = low
699 | tile_sum.none = none
700 |
701 | tiles_by_score = tile_sum.tiles_by_score()
702 | rank = 0
703 | for t in tiles_by_score:
704 | rank += 1
705 | t.rank = rank
706 |
707 | return tile_sum
708 |
709 |
710 | def score_tile(np_tile, tissue_percent, slide_num, row, col):
711 | """
712 | Score tile based on tissue percentage, color factor, saturation/value factor, and tissue quantity factor.
713 |
714 | Args:
715 | np_tile: Tile as NumPy array.
716 | tissue_percent: The percentage of the tile judged to be tissue.
717 | slide_num: Slide number.
718 | row: Tile row.
719 | col: Tile column.
720 |
721 | Returns tuple consisting of score, color factor, saturation/value factor, and tissue quantity factor.
722 | """
723 | color_factor = hsv_purple_pink_factor(np_tile)
724 | s_and_v_factor = hsv_saturation_and_value_factor(np_tile)
725 | amount = tissue_quantity(tissue_percent)
726 | quantity_factor = tissue_quantity_factor(amount)
727 | combined_factor = color_factor * s_and_v_factor * quantity_factor
728 | score = (tissue_percent ** 2) * np.log(1 + combined_factor) / 1000.0
729 | # scale score to between 0 and 1
730 | score = 1.0 - (10.0 / (10.0 + score))
731 | return score, color_factor, s_and_v_factor, quantity_factor
732 |
733 |
734 | def tissue_quantity_factor(amount):
735 | """
736 | Obtain a scoring factor based on the quantity of tissue in a tile.
737 |
738 | Args:
739 | amount: Tissue amount as a TissueQuantity enum value.
740 |
741 | Returns:
742 | Scoring factor based on the tile tissue quantity.
743 | """
744 | if amount == TissueQuantity.HIGH:
745 | quantity_factor = 1.0
746 | elif amount == TissueQuantity.MEDIUM:
747 | quantity_factor = 0.2
748 | elif amount == TissueQuantity.LOW:
749 | quantity_factor = 0.1
750 | else:
751 | quantity_factor = 0.0
752 | return quantity_factor
753 |
754 |
755 | def tissue_quantity(tissue_percentage):
756 | """
757 | Obtain TissueQuantity enum member (HIGH, MEDIUM, LOW, or NONE) for corresponding tissue percentage.
758 |
759 | Args:
760 | tissue_percentage: The tile tissue percentage.
761 |
762 | Returns:
763 | TissueQuantity enum member (HIGH, MEDIUM, LOW, or NONE).
764 | """
765 | if tissue_percentage >= TISSUE_HIGH_THRESH:
766 | return TissueQuantity.HIGH
767 | elif (tissue_percentage >= TISSUE_LOW_THRESH) and (tissue_percentage < TISSUE_HIGH_THRESH):
768 | return TissueQuantity.MEDIUM
769 | elif (tissue_percentage > 0) and (tissue_percentage < TISSUE_LOW_THRESH):
770 | return TissueQuantity.LOW
771 | else:
772 | return TissueQuantity.NONE
773 |
774 |
775 | def image_list_to_tiles(image_num_list, display=False, save_summary=True, save_data=True, save_top_tiles=True):
776 | """
777 | Generate tile summaries and tiles for a list of images.
778 |
779 | Args:
780 | image_num_list: List of image numbers.
781 | display: If True, display tile summary images to screen.
782 | save_summary: If True, save tile summary images.
783 | save_data: If True, save tile data to csv file.
784 | save_top_tiles: If True, save top tiles to files.
785 | """
786 | tile_summaries_dict = dict()
787 | for slide_num in image_num_list:
788 | tile_summary = summary_and_tiles(slide_num, display, save_summary, save_data, save_top_tiles)
789 | tile_summaries_dict[slide_num] = tile_summary
790 | return image_num_list, tile_summaries_dict
791 |
792 |
793 | def image_range_to_tiles(start_ind, end_ind, display=False, save_summary=True, save_data=True, save_top_tiles=True):
794 | """
795 | Generate tile summaries and tiles for a range of images.
796 |
797 | Args:
798 | start_ind: Starting index (inclusive).
799 | end_ind: Ending index (inclusive).
800 | display: If True, display tile summary images to screen.
801 | save_summary: If True, save tile summary images.
802 | save_data: If True, save tile data to csv file.
803 | save_top_tiles: If True, save top tiles to files.
804 | """
805 | image_num_list = list()
806 | tile_summaries_dict = dict()
807 | for slide_num in range(start_ind, end_ind + 1):
808 | tile_summary = summary_and_tiles(slide_num, display, save_summary, save_data, save_top_tiles)
809 | image_num_list.append(slide_num)
810 | tile_summaries_dict[slide_num] = tile_summary
811 | return image_num_list, tile_summaries_dict
812 |
813 |
814 | def singleprocess_filtered_images_to_tiles(display=False, save_summary=True, save_data=True, save_top_tiles=True,
815 | html=True, image_num_list=None):
816 | """
817 | Generate tile summaries and tiles for training images using a single process.
818 |
819 | Args:
820 | display: If True, display tile summary images to screen.
821 | save_summary: If True, save tile summary images.
822 | save_data: If True, save tile data to csv file.
823 | save_top_tiles: If True, save top tiles to files.
824 | html: If True, generate HTML page to display tiled images
825 | image_num_list: Optionally specify a list of image slide numbers.
826 | """
827 | t = Time()
828 | print("Generating tile summaries\n")
829 |
830 | if image_num_list is not None:
831 | image_num_list, tile_summaries_dict = image_list_to_tiles(image_num_list, display, save_summary, save_data,
832 | save_top_tiles)
833 | else:
834 | num_training_slides = slide.get_num_training_slides()
835 | image_num_list, tile_summaries_dict = image_range_to_tiles(1, num_training_slides, display, save_summary, save_data,
836 | save_top_tiles)
837 |
838 | print("Time to generate tile summaries: %s\n" % str(t.elapsed()))
839 |
840 | if html:
841 | generate_tiled_html_result(image_num_list, tile_summaries_dict, save_data)
842 |
843 |
844 | def multiprocess_filtered_images_to_tiles(display=False, save_summary=True, save_data=True, save_top_tiles=True,
845 | html=True, image_num_list=None):
846 | """
847 | Generate tile summaries and tiles for all training images using multiple processes (one process per core).
848 |
849 | Args:
850 | display: If True, display images to screen (multiprocessed display not recommended).
851 | save_summary: If True, save tile summary images.
852 | save_data: If True, save tile data to csv file.
853 | save_top_tiles: If True, save top tiles to files.
854 | html: If True, generate HTML page to display tiled images.
855 | image_num_list: Optionally specify a list of image slide numbers.
856 | """
857 | timer = Time()
858 | print("Generating tile summaries (multiprocess)\n")
859 |
860 | if save_summary and not os.path.exists(slide.TILE_SUMMARY_DIR):
861 | os.makedirs(slide.TILE_SUMMARY_DIR)
862 |
863 | # how many processes to use
864 | num_processes = multiprocessing.cpu_count()
865 | pool = multiprocessing.Pool(num_processes)
866 |
867 | if image_num_list is not None:
868 | num_train_images = len(image_num_list)
869 | else:
870 | num_train_images = slide.get_num_training_slides()
871 | if num_processes > num_train_images:
872 | num_processes = num_train_images
873 | images_per_process = num_train_images / num_processes
874 |
875 | print("Number of processes: " + str(num_processes))
876 | print("Number of training images: " + str(num_train_images))
877 |
878 | tasks = []
879 | for num_process in range(1, num_processes + 1):
880 | start_index = (num_process - 1) * images_per_process + 1
881 | end_index = num_process * images_per_process
882 | start_index = int(start_index)
883 | end_index = int(end_index)
884 | if image_num_list is not None:
885 | sublist = image_num_list[start_index - 1:end_index]
886 | tasks.append((sublist, display, save_summary, save_data, save_top_tiles))
887 | print("Task #" + str(num_process) + ": Process slides " + str(sublist))
888 | else:
889 | tasks.append((start_index, end_index, display, save_summary, save_data, save_top_tiles))
890 | if start_index == end_index:
891 | print("Task #" + str(num_process) + ": Process slide " + str(start_index))
892 | else:
893 | print("Task #" + str(num_process) + ": Process slides " + str(start_index) + " to " + str(end_index))
894 |
895 | # start tasks
896 | results = []
897 | for t in tasks:
898 | if image_num_list is not None:
899 | results.append(pool.apply_async(image_list_to_tiles, t))
900 | else:
901 | results.append(pool.apply_async(image_range_to_tiles, t))
902 |
903 | slide_nums = list()
904 | tile_summaries_dict = dict()
905 | for result in results:
906 | image_nums, tile_summaries = result.get()
907 | slide_nums.extend(image_nums)
908 | tile_summaries_dict.update(tile_summaries)
909 | print("Done tiling slides: %s" % image_nums)
910 |
911 | if html:
912 | generate_tiled_html_result(slide_nums, tile_summaries_dict, save_data)
913 |
914 | print("Time to generate tile previews (multiprocess): %s\n" % str(timer.elapsed()))
915 |
916 |
917 | def image_row(slide_num, tile_summary, data_link):
918 | """
919 | Generate HTML for viewing a tiled image.
920 |
921 | Args:
922 | slide_num: The slide number.
923 | tile_summary: TileSummary object.
924 | data_link: If True, add link to tile data csv file.
925 |
926 | Returns:
927 | HTML table row for viewing a tiled image.
928 | """
929 | orig_img = slide.get_training_image_path(slide_num)
930 | orig_thumb = slide.get_training_thumbnail_path(slide_num)
931 | filt_img = slide.get_filter_image_result(slide_num)
932 | filt_thumb = slide.get_filter_thumbnail_result(slide_num)
933 | sum_img = slide.get_tile_summary_image_path(slide_num)
934 | sum_thumb = slide.get_tile_summary_thumbnail_path(slide_num)
935 | osum_img = slide.get_tile_summary_on_original_image_path(slide_num)
936 | osum_thumb = slide.get_tile_summary_on_original_thumbnail_path(slide_num)
937 | top_img = slide.get_top_tiles_image_path(slide_num)
938 | top_thumb = slide.get_top_tiles_thumbnail_path(slide_num)
939 | otop_img = slide.get_top_tiles_on_original_image_path(slide_num)
940 | otop_thumb = slide.get_top_tiles_on_original_thumbnail_path(slide_num)
941 | html = " \n" + \
942 | " \n" + \
943 | " S%03d Original \n" % (orig_img, slide_num) + \
944 | " \n" % (orig_thumb) + \
945 | " \n" + \
946 | " | \n" + \
947 | " \n" + \
948 | " S%03d Filtered \n" % (filt_img, slide_num) + \
949 | " \n" % (filt_thumb) + \
950 | " \n" + \
951 | " | \n"
952 |
953 | html += " \n" + \
954 | " S%03d Tiles \n" % (sum_img, slide_num) + \
955 | " \n" % (sum_thumb) + \
956 | " \n" + \
957 | " | \n"
958 |
959 | html += " \n" + \
960 | " S%03d Tiles \n" % (osum_img, slide_num) + \
961 | " \n" % (osum_thumb) + \
962 | " \n" + \
963 | " | \n"
964 |
965 | html += " \n"
966 | if data_link:
967 | html += " S%03d Tile Summary\n" % slide_num + \
968 | " ( Data) \n" % slide.get_tile_data_path(slide_num)
969 | else:
970 | html += " S%03d Tile Summary \n" % slide_num
971 |
972 | html += " \n" + \
973 | " %s\n" % summary_stats(tile_summary).replace("\n", " \n ") + \
974 | " \n" + \
975 | " | \n"
976 |
977 | html += " \n" + \
978 | " S%03d Top Tiles \n" % (top_img, slide_num) + \
979 | " \n" % (top_thumb) + \
980 | " \n" + \
981 | " | \n"
982 |
983 | html += " \n" + \
984 | " S%03d Top Tiles \n" % (otop_img, slide_num) + \
985 | " \n" % (otop_thumb) + \
986 | " \n" + \
987 | " | \n"
988 |
989 | top_tiles = tile_summary.top_tiles()
990 | num_tiles = len(top_tiles)
991 | score_num = 0
992 | for t in top_tiles:
993 | score_num += 1
994 | t.tile_num = score_num
995 | # sort top tiles by rows and columns to make them easier to locate on HTML page
996 | top_tiles = sorted(top_tiles, key=lambda t: (t.r, t.c), reverse=False)
997 |
998 | html += " \n" + \
999 | " S%03d Top %d Tile Scores \n" % (slide_num, num_tiles) + \
1000 | " \n"
1001 |
1002 | html += " \n"
1003 | MAX_TILES_PER_ROW = 15
1004 | num_cols = math.ceil(num_tiles / MAX_TILES_PER_ROW)
1005 | num_rows = num_tiles if num_tiles < MAX_TILES_PER_ROW else MAX_TILES_PER_ROW
1006 | for row in range(num_rows):
1007 | html += " \n"
1008 | for col in range(num_cols):
1009 | html += " "
1010 | tile_num = row + (col * num_rows) + 1
1011 | if tile_num <= num_tiles:
1012 | t = top_tiles[tile_num - 1]
1013 | label = "R%03d C%03d %0.4f (#%02d)" % (t.r, t.c, t.score, t.tile_num)
1014 | tile_img_path = slide.get_tile_image_path(t)
1015 | html += "%s" % (tile_img_path, label)
1016 | else:
1017 | html += " "
1018 | html += " | \n"
1019 | html += " \n"
1020 | html += " \n"
1021 |
1022 | html += " \n"
1023 | html += " | \n"
1024 |
1025 | html += "
\n"
1026 | return html
1027 |
1028 |
1029 | def generate_tiled_html_result(slide_nums, tile_summaries_dict, data_link):
1030 | """
1031 | Generate HTML to view the tiled images.
1032 |
1033 | Args:
1034 | slide_nums: List of slide numbers.
1035 | tile_summaries_dict: Dictionary of TileSummary objects keyed by slide number.
1036 | data_link: If True, add link to tile data csv file.
1037 | """
1038 | slide_nums = sorted(slide_nums)
1039 | if not slide.TILE_SUMMARY_PAGINATE:
1040 | html = ""
1041 | html += filter.html_header("Tiles")
1042 |
1043 | html += " \n"
1044 | for slide_num in slide_nums:
1045 | html += image_row(slide_num, data_link)
1046 | html += "
\n"
1047 |
1048 | html += filter.html_footer()
1049 | text_file = open(os.path.join(slide.TILE_SUMMARY_HTML_DIR, "tiles.html"), "w")
1050 | text_file.write(html)
1051 | text_file.close()
1052 | else:
1053 | total_len = len(slide_nums)
1054 | page_size = slide.TILE_SUMMARY_PAGINATION_SIZE
1055 | num_pages = math.ceil(total_len / page_size)
1056 | for page_num in range(1, num_pages + 1):
1057 | start_index = (page_num - 1) * page_size
1058 | end_index = (page_num * page_size) if (page_num < num_pages) else total_len
1059 | page_slide_nums = slide_nums[start_index:end_index]
1060 |
1061 | html = ""
1062 | html += filter.html_header("Tiles, Page %d" % page_num)
1063 |
1064 | html += " "
1065 | if page_num > 1:
1066 | if page_num == 2:
1067 | html += "
< "
1068 | else:
1069 | html += "
< " % (page_num - 1)
1070 | html += "Page %d" % page_num
1071 | if page_num < num_pages:
1072 | html += "
> " % (page_num + 1)
1073 | html += "
\n"
1074 |
1075 | html += " \n"
1076 | for slide_num in page_slide_nums:
1077 | tile_summary = tile_summaries_dict[slide_num]
1078 | html += image_row(slide_num, tile_summary, data_link)
1079 | html += "
\n"
1080 |
1081 | html += filter.html_footer()
1082 | if page_num == 1:
1083 | text_file = open(os.path.join(slide.TILE_SUMMARY_HTML_DIR, "tiles.html"), "w")
1084 | else:
1085 | text_file = open(os.path.join(slide.TILE_SUMMARY_HTML_DIR, "tiles-%d.html" % page_num), "w")
1086 | text_file.write(html)
1087 | text_file.close()
1088 |
1089 |
1090 | def np_hsv_hue_histogram(h):
1091 | """
1092 | Create Matplotlib histogram of hue values for an HSV image and return the histogram as a NumPy array image.
1093 |
1094 | Args:
1095 | h: Hue values as a 1-dimensional int NumPy array (scaled 0 to 360)
1096 |
1097 | Returns:
1098 | Matplotlib histogram of hue values converted to a NumPy array image.
1099 | """
1100 | figure = plt.figure()
1101 | canvas = figure.canvas
1102 | _, _, patches = plt.hist(h, bins=360)
1103 | plt.title("HSV Hue Histogram, mean=%3.1f, std=%3.1f" % (np.mean(h), np.std(h)))
1104 |
1105 | bin_num = 0
1106 | for patch in patches:
1107 | rgb_color = colorsys.hsv_to_rgb(bin_num / 360.0, 1, 1)
1108 | patch.set_facecolor(rgb_color)
1109 | bin_num += 1
1110 |
1111 | canvas.draw()
1112 | w, h = canvas.get_width_height()
1113 | np_hist = np.fromstring(canvas.get_renderer().tostring_rgb(), dtype=np.uint8).reshape(h, w, 3)
1114 | plt.close(figure)
1115 | util.np_info(np_hist)
1116 | return np_hist
1117 |
1118 |
1119 | def np_histogram(data, title, bins="auto"):
1120 | """
1121 | Create Matplotlib histogram and return it as a NumPy array image.
1122 |
1123 | Args:
1124 | data: Data to plot in the histogram.
1125 | title: Title of the histogram.
1126 | bins: Number of histogram bins, "auto" by default.
1127 |
1128 | Returns:
1129 | Matplotlib histogram as a NumPy array image.
1130 | """
1131 | figure = plt.figure()
1132 | canvas = figure.canvas
1133 | plt.hist(data, bins=bins)
1134 | plt.title(title)
1135 |
1136 | canvas.draw()
1137 | w, h = canvas.get_width_height()
1138 | np_hist = np.fromstring(canvas.get_renderer().tostring_rgb(), dtype=np.uint8).reshape(h, w, 3)
1139 | plt.close(figure)
1140 | util.np_info(np_hist)
1141 | return np_hist
1142 |
1143 |
1144 | def np_hsv_saturation_histogram(s):
1145 | """
1146 | Create Matplotlib histogram of saturation values for an HSV image and return the histogram as a NumPy array image.
1147 |
1148 | Args:
1149 | s: Saturation values as a 1-dimensional float NumPy array
1150 |
1151 | Returns:
1152 | Matplotlib histogram of saturation values converted to a NumPy array image.
1153 | """
1154 | title = "HSV Saturation Histogram, mean=%.2f, std=%.2f" % (np.mean(s), np.std(s))
1155 | return np_histogram(s, title)
1156 |
1157 |
1158 | def np_hsv_value_histogram(v):
1159 | """
1160 | Create Matplotlib histogram of value values for an HSV image and return the histogram as a NumPy array image.
1161 |
1162 | Args:
1163 | v: Value values as a 1-dimensional float NumPy array
1164 |
1165 | Returns:
1166 | Matplotlib histogram of saturation values converted to a NumPy array image.
1167 | """
1168 | title = "HSV Value Histogram, mean=%.2f, std=%.2f" % (np.mean(v), np.std(v))
1169 | return np_histogram(v, title)
1170 |
1171 |
1172 | def np_rgb_channel_histogram(rgb, ch_num, ch_name):
1173 | """
1174 | Create Matplotlib histogram of an RGB channel for an RGB image and return the histogram as a NumPy array image.
1175 |
1176 | Args:
1177 | rgb: Image as RGB NumPy array.
1178 | ch_num: Which channel (0=red, 1=green, 2=blue)
1179 | ch_name: Channel name ("R", "G", "B")
1180 |
1181 | Returns:
1182 | Matplotlib histogram of RGB channel converted to a NumPy array image.
1183 | """
1184 |
1185 | ch = rgb[:, :, ch_num]
1186 | ch = ch.flatten()
1187 | title = "RGB %s Histogram, mean=%.2f, std=%.2f" % (ch_name, np.mean(ch), np.std(ch))
1188 | return np_histogram(ch, title, bins=256)
1189 |
1190 |
1191 | def np_rgb_r_histogram(rgb):
1192 | """
1193 | Obtain RGB R channel histogram as a NumPy array image.
1194 |
1195 | Args:
1196 | rgb: Image as RGB NumPy array.
1197 |
1198 | Returns:
1199 | Histogram of RGB R channel as a NumPy array image.
1200 | """
1201 | hist = np_rgb_channel_histogram(rgb, 0, "R")
1202 | return hist
1203 |
1204 |
1205 | def np_rgb_g_histogram(rgb):
1206 | """
1207 | Obtain RGB G channel histogram as a NumPy array image.
1208 |
1209 | Args:
1210 | rgb: Image as RGB NumPy array.
1211 |
1212 | Returns:
1213 | Histogram of RGB G channel as a NumPy array image.
1214 | """
1215 | hist = np_rgb_channel_histogram(rgb, 1, "G")
1216 | return hist
1217 |
1218 |
1219 | def np_rgb_b_histogram(rgb):
1220 | """
1221 | Obtain RGB B channel histogram as a NumPy array image.
1222 |
1223 | Args:
1224 | rgb: Image as RGB NumPy array.
1225 |
1226 | Returns:
1227 | Histogram of RGB B channel as a NumPy array image.
1228 | """
1229 | hist = np_rgb_channel_histogram(rgb, 2, "B")
1230 | return hist
1231 |
1232 |
1233 | def pil_hue_histogram(h):
1234 | """
1235 | Create Matplotlib histogram of hue values for an HSV image and return the histogram as a PIL image.
1236 |
1237 | Args:
1238 | h: Hue values as a 1-dimensional int NumPy array (scaled 0 to 360)
1239 |
1240 | Returns:
1241 | Matplotlib histogram of hue values converted to a PIL image.
1242 | """
1243 | np_hist = np_hsv_hue_histogram(h)
1244 | pil_hist = util.np_to_pil(np_hist)
1245 | return pil_hist
1246 |
1247 |
1248 | def display_image_with_hsv_hue_histogram(np_rgb, text=None, scale_up=False):
1249 | """
1250 | Display an image with its corresponding hue histogram.
1251 |
1252 | Args:
1253 | np_rgb: RGB image tile as a NumPy array
1254 | text: Optional text to display above image
1255 | scale_up: If True, scale up image to display by slide.SCALE_FACTOR
1256 | """
1257 | hsv = filter.filter_rgb_to_hsv(np_rgb)
1258 | h = filter.filter_hsv_to_h(hsv)
1259 | np_hist = np_hsv_hue_histogram(h)
1260 | hist_r, hist_c, _ = np_hist.shape
1261 |
1262 | if scale_up:
1263 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=1)
1264 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=0)
1265 |
1266 | img_r, img_c, img_ch = np_rgb.shape
1267 | if text is not None:
1268 | np_t = np_text(text)
1269 | t_r, t_c, _ = np_t.shape
1270 | t_i_c = max(t_c, img_c)
1271 | t_i_r = t_r + img_r
1272 | t_i = np.zeros([t_i_r, t_i_c, img_ch], dtype=np.uint8)
1273 | t_i.fill(255)
1274 | t_i[0:t_r, 0:t_c] = np_t
1275 | t_i[t_r:t_r + img_r, 0:img_c] = np_rgb
1276 | np_rgb = t_i # for simplicity assign title+image to image
1277 | img_r, img_c, img_ch = np_rgb.shape
1278 |
1279 | r = max(img_r, hist_r)
1280 | c = img_c + hist_c
1281 | combo = np.zeros([r, c, img_ch], dtype=np.uint8)
1282 | combo.fill(255)
1283 | combo[0:img_r, 0:img_c] = np_rgb
1284 | combo[0:hist_r, img_c:c] = np_hist
1285 | pil_combo = util.np_to_pil(combo)
1286 | pil_combo.show()
1287 |
1288 |
1289 | def display_image(np_rgb, text=None, scale_up=False):
1290 | """
1291 | Display an image with optional text above image.
1292 |
1293 | Args:
1294 | np_rgb: RGB image tile as a NumPy array
1295 | text: Optional text to display above image
1296 | scale_up: If True, scale up image to display by slide.SCALE_FACTOR
1297 | """
1298 | if scale_up:
1299 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=1)
1300 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=0)
1301 |
1302 | img_r, img_c, img_ch = np_rgb.shape
1303 | if text is not None:
1304 | np_t = np_text(text)
1305 | t_r, t_c, _ = np_t.shape
1306 | t_i_c = max(t_c, img_c)
1307 | t_i_r = t_r + img_r
1308 | t_i = np.zeros([t_i_r, t_i_c, img_ch], dtype=np.uint8)
1309 | t_i.fill(255)
1310 | t_i[0:t_r, 0:t_c] = np_t
1311 | t_i[t_r:t_r + img_r, 0:img_c] = np_rgb
1312 | np_rgb = t_i
1313 |
1314 | pil_img = util.np_to_pil(np_rgb)
1315 | pil_img.show()
1316 |
1317 |
1318 | def display_image_with_hsv_histograms(np_rgb, text=None, scale_up=False):
1319 | """
1320 | Display an image with its corresponding HSV hue, saturation, and value histograms.
1321 |
1322 | Args:
1323 | np_rgb: RGB image tile as a NumPy array
1324 | text: Optional text to display above image
1325 | scale_up: If True, scale up image to display by slide.SCALE_FACTOR
1326 | """
1327 | hsv = filter.filter_rgb_to_hsv(np_rgb)
1328 | np_h = np_hsv_hue_histogram(filter.filter_hsv_to_h(hsv))
1329 | np_s = np_hsv_saturation_histogram(filter.filter_hsv_to_s(hsv))
1330 | np_v = np_hsv_value_histogram(filter.filter_hsv_to_v(hsv))
1331 | h_r, h_c, _ = np_h.shape
1332 | s_r, s_c, _ = np_s.shape
1333 | v_r, v_c, _ = np_v.shape
1334 |
1335 | if scale_up:
1336 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=1)
1337 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=0)
1338 |
1339 | img_r, img_c, img_ch = np_rgb.shape
1340 | if text is not None:
1341 | np_t = np_text(text)
1342 | t_r, t_c, _ = np_t.shape
1343 | t_i_c = max(t_c, img_c)
1344 | t_i_r = t_r + img_r
1345 | t_i = np.zeros([t_i_r, t_i_c, img_ch], dtype=np.uint8)
1346 | t_i.fill(255)
1347 | t_i[0:t_r, 0:t_c] = np_t
1348 | t_i[t_r:t_r + img_r, 0:img_c] = np_rgb
1349 | np_rgb = t_i # for simplicity assign title+image to image
1350 | img_r, img_c, img_ch = np_rgb.shape
1351 |
1352 | hists_c = max(h_c, s_c, v_c)
1353 | hists_r = h_r + s_r + v_r
1354 | hists = np.zeros([hists_r, hists_c, img_ch], dtype=np.uint8)
1355 |
1356 | hists[0:h_r, 0:h_c] = np_h
1357 | hists[h_r:h_r + s_r, 0:s_c] = np_s
1358 | hists[h_r + s_r:h_r + s_r + v_r, 0:v_c] = np_v
1359 |
1360 | r = max(img_r, hists_r)
1361 | c = img_c + hists_c
1362 | combo = np.zeros([r, c, img_ch], dtype=np.uint8)
1363 | combo.fill(255)
1364 | combo[0:img_r, 0:img_c] = np_rgb
1365 | combo[0:hists_r, img_c:c] = hists
1366 | pil_combo = util.np_to_pil(combo)
1367 | pil_combo.show()
1368 |
1369 |
1370 | def display_image_with_rgb_histograms(np_rgb, text=None, scale_up=False):
1371 | """
1372 | Display an image with its corresponding RGB histograms.
1373 |
1374 | Args:
1375 | np_rgb: RGB image tile as a NumPy array
1376 | text: Optional text to display above image
1377 | scale_up: If True, scale up image to display by slide.SCALE_FACTOR
1378 | """
1379 | np_r = np_rgb_r_histogram(np_rgb)
1380 | np_g = np_rgb_g_histogram(np_rgb)
1381 | np_b = np_rgb_b_histogram(np_rgb)
1382 | r_r, r_c, _ = np_r.shape
1383 | g_r, g_c, _ = np_g.shape
1384 | b_r, b_c, _ = np_b.shape
1385 |
1386 | if scale_up:
1387 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=1)
1388 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=0)
1389 |
1390 | img_r, img_c, img_ch = np_rgb.shape
1391 | if text is not None:
1392 | np_t = np_text(text)
1393 | t_r, t_c, _ = np_t.shape
1394 | t_i_c = max(t_c, img_c)
1395 | t_i_r = t_r + img_r
1396 | t_i = np.zeros([t_i_r, t_i_c, img_ch], dtype=np.uint8)
1397 | t_i.fill(255)
1398 | t_i[0:t_r, 0:t_c] = np_t
1399 | t_i[t_r:t_r + img_r, 0:img_c] = np_rgb
1400 | np_rgb = t_i # for simplicity assign title+image to image
1401 | img_r, img_c, img_ch = np_rgb.shape
1402 |
1403 | hists_c = max(r_c, g_c, b_c)
1404 | hists_r = r_r + g_r + b_r
1405 | hists = np.zeros([hists_r, hists_c, img_ch], dtype=np.uint8)
1406 |
1407 | hists[0:r_r, 0:r_c] = np_r
1408 | hists[r_r:r_r + g_r, 0:g_c] = np_g
1409 | hists[r_r + g_r:r_r + g_r + b_r, 0:b_c] = np_b
1410 |
1411 | r = max(img_r, hists_r)
1412 | c = img_c + hists_c
1413 | combo = np.zeros([r, c, img_ch], dtype=np.uint8)
1414 | combo.fill(255)
1415 | combo[0:img_r, 0:img_c] = np_rgb
1416 | combo[0:hists_r, img_c:c] = hists
1417 | pil_combo = util.np_to_pil(combo)
1418 | pil_combo.show()
1419 |
1420 |
1421 | def pil_text(text, w_border=TILE_TEXT_W_BORDER, h_border=TILE_TEXT_H_BORDER, font_path=FONT_PATH,
1422 | font_size=TILE_TEXT_SIZE, text_color=TILE_TEXT_COLOR, background=TILE_TEXT_BACKGROUND_COLOR):
1423 | """
1424 | Obtain a PIL image representation of text.
1425 |
1426 | Args:
1427 | text: The text to convert to an image.
1428 | w_border: Tile text width border (left and right).
1429 | h_border: Tile text height border (top and bottom).
1430 | font_path: Path to font.
1431 | font_size: Size of font.
1432 | text_color: Tile text color.
1433 | background: Tile background color.
1434 |
1435 | Returns:
1436 | PIL image representing the text.
1437 | """
1438 |
1439 | font = ImageFont.truetype(font_path, font_size)
1440 | x, y = ImageDraw.Draw(Image.new("RGB", (1, 1), background)).textsize(text, font)
1441 | image = Image.new("RGB", (x + 2 * w_border, y + 2 * h_border), background)
1442 | draw = ImageDraw.Draw(image)
1443 | draw.text((w_border, h_border), text, text_color, font=font)
1444 | return image
1445 |
1446 |
1447 | def np_text(text, w_border=TILE_TEXT_W_BORDER, h_border=TILE_TEXT_H_BORDER, font_path=FONT_PATH,
1448 | font_size=TILE_TEXT_SIZE, text_color=TILE_TEXT_COLOR, background=TILE_TEXT_BACKGROUND_COLOR):
1449 | """
1450 | Obtain a NumPy array image representation of text.
1451 |
1452 | Args:
1453 | text: The text to convert to an image.
1454 | w_border: Tile text width border (left and right).
1455 | h_border: Tile text height border (top and bottom).
1456 | font_path: Path to font.
1457 | font_size: Size of font.
1458 | text_color: Tile text color.
1459 | background: Tile background color.
1460 |
1461 | Returns:
1462 | NumPy array representing the text.
1463 | """
1464 | pil_img = pil_text(text, w_border, h_border, font_path, font_size,
1465 | text_color, background)
1466 | np_img = util.pil_to_np_rgb(pil_img)
1467 | return np_img
1468 |
1469 |
1470 | def display_tile(tile, rgb_histograms=True, hsv_histograms=True):
1471 | """
1472 | Display a tile with its corresponding RGB and HSV histograms.
1473 |
1474 | Args:
1475 | tile: The Tile object.
1476 | rgb_histograms: If True, display RGB histograms.
1477 | hsv_histograms: If True, display HSV histograms.
1478 | """
1479 |
1480 | text = "S%03d R%03d C%03d\n" % (tile.slide_num, tile.r, tile.c)
1481 | text += "Score:%4.2f Tissue:%5.2f%% CF:%2.0f SVF:%4.2f QF:%4.2f\n" % (
1482 | tile.score, tile.tissue_percentage, tile.color_factor, tile.s_and_v_factor, tile.quantity_factor)
1483 | text += "Rank #%d of %d" % (tile.rank, tile.tile_summary.num_tiles())
1484 |
1485 | np_scaled_tile = tile.get_np_scaled_tile()
1486 | if np_scaled_tile is not None:
1487 | small_text = text + "\n \nSmall Tile (%d x %d)" % (np_scaled_tile.shape[1], np_scaled_tile.shape[0])
1488 | if rgb_histograms and hsv_histograms:
1489 | display_image_with_rgb_and_hsv_histograms(np_scaled_tile, small_text, scale_up=True)
1490 | elif rgb_histograms:
1491 | display_image_with_rgb_histograms(np_scaled_tile, small_text, scale_up=True)
1492 | elif hsv_histograms:
1493 | display_image_with_hsv_histograms(np_scaled_tile, small_text, scale_up=True)
1494 | else:
1495 | display_image(np_scaled_tile, small_text, scale_up=True)
1496 |
1497 | np_tile = tile.get_np_tile()
1498 | text += " based on small tile\n \nLarge Tile (%d x %d)" % (np_tile.shape[1], np_tile.shape[0])
1499 | if rgb_histograms and hsv_histograms:
1500 | display_image_with_rgb_and_hsv_histograms(np_tile, text)
1501 | elif rgb_histograms:
1502 | display_image_with_rgb_histograms(np_tile, text)
1503 | elif hsv_histograms:
1504 | display_image_with_hsv_histograms(np_tile, text)
1505 | else:
1506 | display_image(np_tile, text)
1507 |
1508 |
1509 | def display_image_with_rgb_and_hsv_histograms(np_rgb, text=None, scale_up=False):
1510 | """
1511 | Display a tile with its corresponding RGB and HSV histograms.
1512 |
1513 | Args:
1514 | np_rgb: RGB image tile as a NumPy array
1515 | text: Optional text to display above image
1516 | scale_up: If True, scale up image to display by slide.SCALE_FACTOR
1517 | """
1518 | hsv = filter.filter_rgb_to_hsv(np_rgb)
1519 | np_r = np_rgb_r_histogram(np_rgb)
1520 | np_g = np_rgb_g_histogram(np_rgb)
1521 | np_b = np_rgb_b_histogram(np_rgb)
1522 | np_h = np_hsv_hue_histogram(filter.filter_hsv_to_h(hsv))
1523 | np_s = np_hsv_saturation_histogram(filter.filter_hsv_to_s(hsv))
1524 | np_v = np_hsv_value_histogram(filter.filter_hsv_to_v(hsv))
1525 |
1526 | r_r, r_c, _ = np_r.shape
1527 | g_r, g_c, _ = np_g.shape
1528 | b_r, b_c, _ = np_b.shape
1529 | h_r, h_c, _ = np_h.shape
1530 | s_r, s_c, _ = np_s.shape
1531 | v_r, v_c, _ = np_v.shape
1532 |
1533 | if scale_up:
1534 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=1)
1535 | np_rgb = np.repeat(np_rgb, slide.SCALE_FACTOR, axis=0)
1536 |
1537 | img_r, img_c, img_ch = np_rgb.shape
1538 | if text is not None:
1539 | np_t = np_text(text)
1540 | t_r, t_c, _ = np_t.shape
1541 | t_i_c = max(t_c, img_c)
1542 | t_i_r = t_r + img_r
1543 | t_i = np.zeros([t_i_r, t_i_c, img_ch], dtype=np.uint8)
1544 | t_i.fill(255)
1545 | t_i[0:t_r, 0:t_c] = np_t
1546 | t_i[t_r:t_r + img_r, 0:img_c] = np_rgb
1547 | np_rgb = t_i # for simplicity assign title+image to image
1548 | img_r, img_c, img_ch = np_rgb.shape
1549 |
1550 | rgb_hists_c = max(r_c, g_c, b_c)
1551 | rgb_hists_r = r_r + g_r + b_r
1552 | rgb_hists = np.zeros([rgb_hists_r, rgb_hists_c, img_ch], dtype=np.uint8)
1553 | rgb_hists[0:r_r, 0:r_c] = np_r
1554 | rgb_hists[r_r:r_r + g_r, 0:g_c] = np_g
1555 | rgb_hists[r_r + g_r:r_r + g_r + b_r, 0:b_c] = np_b
1556 |
1557 | hsv_hists_c = max(h_c, s_c, v_c)
1558 | hsv_hists_r = h_r + s_r + v_r
1559 | hsv_hists = np.zeros([hsv_hists_r, hsv_hists_c, img_ch], dtype=np.uint8)
1560 | hsv_hists[0:h_r, 0:h_c] = np_h
1561 | hsv_hists[h_r:h_r + s_r, 0:s_c] = np_s
1562 | hsv_hists[h_r + s_r:h_r + s_r + v_r, 0:v_c] = np_v
1563 |
1564 | r = max(img_r, rgb_hists_r, hsv_hists_r)
1565 | c = img_c + rgb_hists_c + hsv_hists_c
1566 | combo = np.zeros([r, c, img_ch], dtype=np.uint8)
1567 | combo.fill(255)
1568 | combo[0:img_r, 0:img_c] = np_rgb
1569 | combo[0:rgb_hists_r, img_c:img_c + rgb_hists_c] = rgb_hists
1570 | combo[0:hsv_hists_r, img_c + rgb_hists_c:c] = hsv_hists
1571 | pil_combo = util.np_to_pil(combo)
1572 | pil_combo.show()
1573 |
1574 |
1575 | def rgb_to_hues(rgb):
1576 | """
1577 | Convert RGB NumPy array to 1-dimensional array of hue values (HSV H values in degrees).
1578 |
1579 | Args:
1580 | rgb: RGB image as a NumPy array
1581 |
1582 | Returns:
1583 | 1-dimensional array of hue values in degrees
1584 | """
1585 | hsv = filter.filter_rgb_to_hsv(rgb, display_np_info=False)
1586 | h = filter.filter_hsv_to_h(hsv, display_np_info=False)
1587 | return h
1588 |
1589 |
1590 | def hsv_saturation_and_value_factor(rgb):
1591 | """
1592 | Function to reduce scores of tiles with narrow HSV saturations and values since saturation and value standard
1593 | deviations should be relatively broad if the tile contains significant tissue.
1594 |
1595 | Example of a blurred tile that should not be ranked as a top tile:
1596 | ../data/tiles_png/006/TUPAC-TR-006-tile-r58-c3-x2048-y58369-w1024-h1024.png
1597 |
1598 | Args:
1599 | rgb: RGB image as a NumPy array
1600 |
1601 | Returns:
1602 | Saturation and value factor, where 1 is no effect and less than 1 means the standard deviations of saturation and
1603 | value are relatively small.
1604 | """
1605 | hsv = filter.filter_rgb_to_hsv(rgb, display_np_info=False)
1606 | s = filter.filter_hsv_to_s(hsv)
1607 | v = filter.filter_hsv_to_v(hsv)
1608 | s_std = np.std(s)
1609 | v_std = np.std(v)
1610 | if s_std < 0.05 and v_std < 0.05:
1611 | factor = 0.4
1612 | elif s_std < 0.05:
1613 | factor = 0.7
1614 | elif v_std < 0.05:
1615 | factor = 0.7
1616 | else:
1617 | factor = 1
1618 |
1619 | factor = factor ** 2
1620 | return factor
1621 |
1622 |
1623 | def hsv_purple_deviation(hsv_hues):
1624 | """
1625 | Obtain the deviation from the HSV hue for purple.
1626 |
1627 | Args:
1628 | hsv_hues: NumPy array of HSV hue values.
1629 |
1630 | Returns:
1631 | The HSV purple deviation.
1632 | """
1633 | purple_deviation = np.sqrt(np.mean(np.abs(hsv_hues - HSV_PURPLE) ** 2))
1634 | return purple_deviation
1635 |
1636 |
1637 | def hsv_pink_deviation(hsv_hues):
1638 | """
1639 | Obtain the deviation from the HSV hue for pink.
1640 |
1641 | Args:
1642 | hsv_hues: NumPy array of HSV hue values.
1643 |
1644 | Returns:
1645 | The HSV pink deviation.
1646 | """
1647 | pink_deviation = np.sqrt(np.mean(np.abs(hsv_hues - HSV_PINK) ** 2))
1648 | return pink_deviation
1649 |
1650 |
1651 | def hsv_purple_pink_factor(rgb):
1652 | """
1653 | Compute scoring factor based on purple and pink HSV hue deviations and degree to which a narrowed hue color range
1654 | average is purple versus pink.
1655 |
1656 | Args:
1657 | rgb: Image an NumPy array.
1658 |
1659 | Returns:
1660 | Factor that favors purple (hematoxylin stained) tissue over pink (eosin stained) tissue.
1661 | """
1662 | hues = rgb_to_hues(rgb)
1663 | hues = hues[hues >= 260] # exclude hues under 260
1664 | hues = hues[hues <= 340] # exclude hues over 340
1665 | if len(hues) == 0:
1666 | return 0 # if no hues between 260 and 340, then not purple or pink
1667 | pu_dev = hsv_purple_deviation(hues)
1668 | pi_dev = hsv_pink_deviation(hues)
1669 | avg_factor = (340 - np.average(hues)) ** 2
1670 |
1671 | if pu_dev == 0: # avoid divide by zero if tile has no tissue
1672 | return 0
1673 |
1674 | factor = pi_dev / pu_dev * avg_factor
1675 | return factor
1676 |
1677 |
1678 | def hsv_purple_vs_pink_average_factor(rgb, tissue_percentage):
1679 | """
1680 | Function to favor purple (hematoxylin) over pink (eosin) staining based on the distance of the HSV hue average
1681 | from purple and pink.
1682 |
1683 | Args:
1684 | rgb: Image as RGB NumPy array
1685 | tissue_percentage: Amount of tissue on the tile
1686 |
1687 | Returns:
1688 | Factor, where >1 to boost purple slide scores, <1 to reduce pink slide scores, or 1 no effect.
1689 | """
1690 |
1691 | factor = 1
1692 | # only applies to slides with a high quantity of tissue
1693 | if tissue_percentage < TISSUE_HIGH_THRESH:
1694 | return factor
1695 |
1696 | hues = rgb_to_hues(rgb)
1697 | hues = hues[hues >= 200] # Remove hues under 200
1698 | if len(hues) == 0:
1699 | return factor
1700 | avg = np.average(hues)
1701 | # pil_hue_histogram(hues).show()
1702 |
1703 | pu = HSV_PURPLE - avg
1704 | pi = HSV_PINK - avg
1705 | pupi = pu + pi
1706 | # print("Av: %4d, Pu: %4d, Pi: %4d, PuPi: %4d" % (avg, pu, pi, pupi))
1707 | # Av: 250, Pu: 20, Pi: 80, PuPi: 100
1708 | # Av: 260, Pu: 10, Pi: 70, PuPi: 80
1709 | # Av: 270, Pu: 0, Pi: 60, PuPi: 60 ** PURPLE
1710 | # Av: 280, Pu: -10, Pi: 50, PuPi: 40
1711 | # Av: 290, Pu: -20, Pi: 40, PuPi: 20
1712 | # Av: 300, Pu: -30, Pi: 30, PuPi: 0
1713 | # Av: 310, Pu: -40, Pi: 20, PuPi: -20
1714 | # Av: 320, Pu: -50, Pi: 10, PuPi: -40
1715 | # Av: 330, Pu: -60, Pi: 0, PuPi: -60 ** PINK
1716 | # Av: 340, Pu: -70, Pi: -10, PuPi: -80
1717 | # Av: 350, Pu: -80, Pi: -20, PuPi: -100
1718 |
1719 | if pupi > 30:
1720 | factor *= 1.2
1721 | if pupi < -30:
1722 | factor *= .8
1723 | if pupi > 0:
1724 | factor *= 1.2
1725 | if pupi > 50:
1726 | factor *= 1.2
1727 | if pupi < -60:
1728 | factor *= .8
1729 |
1730 | return factor
1731 |
1732 |
1733 | class TileSummary:
1734 | """
1735 | Class for tile summary information.
1736 | """
1737 |
1738 | slide_num = None
1739 | orig_w = None
1740 | orig_h = None
1741 | orig_tile_w = None
1742 | orig_tile_h = None
1743 | scale_factor = slide.SCALE_FACTOR
1744 | scaled_w = None
1745 | scaled_h = None
1746 | scaled_tile_w = None
1747 | scaled_tile_h = None
1748 | mask_percentage = None
1749 | num_row_tiles = None
1750 | num_col_tiles = None
1751 |
1752 | count = 0
1753 | high = 0
1754 | medium = 0
1755 | low = 0
1756 | none = 0
1757 |
1758 | def __init__(self, slide_num, orig_w, orig_h, orig_tile_w, orig_tile_h, scaled_w, scaled_h, scaled_tile_w,
1759 | scaled_tile_h, tissue_percentage, num_col_tiles, num_row_tiles):
1760 | self.slide_num = slide_num
1761 | self.orig_w = orig_w
1762 | self.orig_h = orig_h
1763 | self.orig_tile_w = orig_tile_w
1764 | self.orig_tile_h = orig_tile_h
1765 | self.scaled_w = scaled_w
1766 | self.scaled_h = scaled_h
1767 | self.scaled_tile_w = scaled_tile_w
1768 | self.scaled_tile_h = scaled_tile_h
1769 | self.tissue_percentage = tissue_percentage
1770 | self.num_col_tiles = num_col_tiles
1771 | self.num_row_tiles = num_row_tiles
1772 | self.tiles = []
1773 |
1774 | def __str__(self):
1775 | return summary_title(self) + "\n" + summary_stats(self)
1776 |
1777 | def mask_percentage(self):
1778 | """
1779 | Obtain the percentage of the slide that is masked.
1780 |
1781 | Returns:
1782 | The amount of the slide that is masked as a percentage.
1783 | """
1784 | return 100 - self.tissue_percentage
1785 |
1786 | def num_tiles(self):
1787 | """
1788 | Retrieve the total number of tiles.
1789 |
1790 | Returns:
1791 | The total number of tiles (number of rows * number of columns).
1792 | """
1793 | return self.num_row_tiles * self.num_col_tiles
1794 |
1795 | def tiles_by_tissue_percentage(self):
1796 | """
1797 | Retrieve the tiles ranked by tissue percentage.
1798 |
1799 | Returns:
1800 | List of the tiles ranked by tissue percentage.
1801 | """
1802 | sorted_list = sorted(self.tiles, key=lambda t: t.tissue_percentage, reverse=True)
1803 | return sorted_list
1804 |
1805 | def tiles_by_score(self):
1806 | """
1807 | Retrieve the tiles ranked by score.
1808 |
1809 | Returns:
1810 | List of the tiles ranked by score.
1811 | """
1812 | sorted_list = sorted(self.tiles, key=lambda t: t.score, reverse=True)
1813 | return sorted_list
1814 |
1815 | def top_tiles(self):
1816 | """
1817 | Retrieve the top-scoring tiles.
1818 |
1819 | Returns:
1820 | List of the top-scoring tiles.
1821 | """
1822 | sorted_tiles = self.tiles_by_score()
1823 | top_tiles = sorted_tiles[:NUM_TOP_TILES]
1824 | return top_tiles
1825 |
1826 | def get_tile(self, row, col):
1827 | """
1828 | Retrieve tile by row and column.
1829 |
1830 | Args:
1831 | row: The row
1832 | col: The column
1833 |
1834 | Returns:
1835 | Corresponding Tile object.
1836 | """
1837 | tile_index = (row - 1) * self.num_col_tiles + (col - 1)
1838 | tile = self.tiles[tile_index]
1839 | return tile
1840 |
1841 | def display_summaries(self):
1842 | """
1843 | Display summary images.
1844 | """
1845 | summary_and_tiles(self.slide_num, display=True, save_summary=False, save_data=False, save_top_tiles=False)
1846 |
1847 |
1848 | class Tile:
1849 | """
1850 | Class for information about a tile.
1851 | """
1852 |
1853 | def __init__(self, tile_summary, slide_num, np_scaled_tile, tile_num, r, c, r_s, r_e, c_s, c_e, o_r_s, o_r_e, o_c_s,
1854 | o_c_e, t_p, color_factor, s_and_v_factor, quantity_factor, score):
1855 | self.tile_summary = tile_summary
1856 | self.slide_num = slide_num
1857 | self.np_scaled_tile = np_scaled_tile
1858 | self.tile_num = tile_num
1859 | self.r = r
1860 | self.c = c
1861 | self.r_s = r_s
1862 | self.r_e = r_e
1863 | self.c_s = c_s
1864 | self.c_e = c_e
1865 | self.o_r_s = o_r_s
1866 | self.o_r_e = o_r_e
1867 | self.o_c_s = o_c_s
1868 | self.o_c_e = o_c_e
1869 | self.tissue_percentage = t_p
1870 | self.color_factor = color_factor
1871 | self.s_and_v_factor = s_and_v_factor
1872 | self.quantity_factor = quantity_factor
1873 | self.score = score
1874 |
1875 | def __str__(self):
1876 | return "[Tile #%d, Row #%d, Column #%d, Tissue %4.2f%%, Score %0.4f]" % (
1877 | self.tile_num, self.r, self.c, self.tissue_percentage, self.score)
1878 |
1879 | def __repr__(self):
1880 | return "\n" + self.__str__()
1881 |
1882 | def mask_percentage(self):
1883 | return 100 - self.tissue_percentage
1884 |
1885 | def tissue_quantity(self):
1886 | return tissue_quantity(self.tissue_percentage)
1887 |
1888 | def get_pil_tile(self):
1889 | return tile_to_pil_tile(self)
1890 |
1891 | def get_np_tile(self):
1892 | return tile_to_np_tile(self)
1893 |
1894 | def save_tile(self):
1895 | save_display_tile(self, save=True, display=False)
1896 |
1897 | def display_tile(self):
1898 | save_display_tile(self, save=False, display=True)
1899 |
1900 | def display_with_histograms(self):
1901 | display_tile(self, rgb_histograms=True, hsv_histograms=True)
1902 |
1903 | def get_np_scaled_tile(self):
1904 | return self.np_scaled_tile
1905 |
1906 | def get_pil_scaled_tile(self):
1907 | return util.np_to_pil(self.np_scaled_tile)
1908 |
1909 |
1910 | class TissueQuantity(Enum):
1911 | NONE = 0
1912 | LOW = 1
1913 | MEDIUM = 2
1914 | HIGH = 3
1915 |
1916 |
1917 | def dynamic_tiles(slide_num, small_tile_in_tile=False):
1918 | """
1919 | Generate tile summary with top tiles using original WSI training slide without intermediate image files saved to
1920 | file system.
1921 |
1922 | Args:
1923 | slide_num: The slide number.
1924 | small_tile_in_tile: If True, include the small NumPy image in the Tile objects.
1925 |
1926 | Returns:
1927 | TileSummary object with list of top Tile objects. The actual tile images are not retrieved until the
1928 | Tile get_tile() methods are called.
1929 | """
1930 | np_img, large_w, large_h, small_w, small_h = slide.slide_to_scaled_np_image(slide_num)
1931 | filt_np_img = filter.apply_image_filters(np_img)
1932 | tile_summary = score_tiles(slide_num, filt_np_img, (large_w, large_h, small_w, small_h), small_tile_in_tile)
1933 | return tile_summary
1934 |
1935 |
1936 | def dynamic_tile(slide_num, row, col, small_tile_in_tile=False):
1937 | """
1938 | Generate a single tile dynamically based on slide number, row, and column. If more than one tile needs to be
1939 | retrieved dynamically, dynamic_tiles() should be used.
1940 |
1941 | Args:
1942 | slide_num: The slide number.
1943 | row: The row.
1944 | col: The column.
1945 | small_tile_in_tile: If True, include the small NumPy image in the Tile objects.
1946 |
1947 | Returns:
1948 | Tile tile object.
1949 | """
1950 | tile_summary = dynamic_tiles(slide_num, small_tile_in_tile)
1951 | tile = tile_summary.get_tile(row, col)
1952 | return tile
1953 |
1954 | # if __name__ == "__main__":
1955 | # tile = dynamic_tile(2, 29, 16, True)
1956 | # tile.display_with_histograms()
1957 |
1958 | # singleprocess_filtered_images_to_tiles()
1959 | # multiprocess_filtered_images_to_tiles()
1960 |
--------------------------------------------------------------------------------
/deephistopath/wsi/util.py:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 | #
15 | # ------------------------------------------------------------------------
16 |
17 | import datetime
18 | import numpy as np
19 | from PIL import Image, ImageDraw, ImageFont
20 |
21 | # If True, display additional NumPy array stats (min, max, mean, is_binary).
22 | ADDITIONAL_NP_STATS = False
23 |
24 |
25 | def pil_to_np_rgb(pil_img):
26 | """
27 | Convert a PIL Image to a NumPy array.
28 |
29 | Note that RGB PIL (w, h) -> NumPy (h, w, 3).
30 |
31 | Args:
32 | pil_img: The PIL Image.
33 |
34 | Returns:
35 | The PIL image converted to a NumPy array.
36 | """
37 | t = Time()
38 | rgb = np.asarray(pil_img)
39 | np_info(rgb, "RGB", t.elapsed())
40 | return rgb
41 |
42 |
43 | def np_to_pil(np_img):
44 | """
45 | Convert a NumPy array to a PIL Image.
46 |
47 | Args:
48 | np_img: The image represented as a NumPy array.
49 |
50 | Returns:
51 | The NumPy array converted to a PIL Image.
52 | """
53 | if np_img.dtype == "bool":
54 | np_img = np_img.astype("uint8") * 255
55 | elif np_img.dtype == "float64":
56 | np_img = (np_img * 255).astype("uint8")
57 | return Image.fromarray(np_img)
58 |
59 |
60 | def np_info(np_arr, name=None, elapsed=None):
61 | """
62 | Display information (shape, type, max, min, etc) about a NumPy array.
63 |
64 | Args:
65 | np_arr: The NumPy array.
66 | name: The (optional) name of the array.
67 | elapsed: The (optional) time elapsed to perform a filtering operation.
68 | """
69 |
70 | if name is None:
71 | name = "NumPy Array"
72 | if elapsed is None:
73 | elapsed = "---"
74 |
75 | if ADDITIONAL_NP_STATS is False:
76 | print("%-20s | Time: %-14s Type: %-7s Shape: %s" % (name, str(elapsed), np_arr.dtype, np_arr.shape))
77 | else:
78 | # np_arr = np.asarray(np_arr)
79 | max = np_arr.max()
80 | min = np_arr.min()
81 | mean = np_arr.mean()
82 | is_binary = "T" if (np.unique(np_arr).size == 2) else "F"
83 | print("%-20s | Time: %-14s Min: %6.2f Max: %6.2f Mean: %6.2f Binary: %s Type: %-7s Shape: %s" % (
84 | name, str(elapsed), min, max, mean, is_binary, np_arr.dtype, np_arr.shape))
85 |
86 |
87 | def display_img(np_img, text=None, font_path="/Library/Fonts/Arial Bold.ttf", size=48, color=(255, 0, 0),
88 | background=(255, 255, 255), border=(0, 0, 0), bg=False):
89 | """
90 | Convert a NumPy array to a PIL image, add text to the image, and display the image.
91 |
92 | Args:
93 | np_img: Image as a NumPy array.
94 | text: The text to add to the image.
95 | font_path: The path to the font to use.
96 | size: The font size
97 | color: The font color
98 | background: The background color
99 | border: The border color
100 | bg: If True, add rectangle background behind text
101 | """
102 | result = np_to_pil(np_img)
103 | # if gray, convert to RGB for display
104 | if result.mode == 'L':
105 | result = result.convert('RGB')
106 | draw = ImageDraw.Draw(result)
107 | if text is not None:
108 | font = ImageFont.truetype(font_path, size)
109 | if bg:
110 | (x, y) = draw.textsize(text, font)
111 | draw.rectangle([(0, 0), (x + 5, y + 4)], fill=background, outline=border)
112 | draw.text((2, 0), text, color, font=font)
113 | result.show()
114 |
115 |
116 | def mask_rgb(rgb, mask):
117 | """
118 | Apply a binary (T/F, 1/0) mask to a 3-channel RGB image and output the result.
119 |
120 | Args:
121 | rgb: RGB image as a NumPy array.
122 | mask: An image mask to determine which pixels in the original image should be displayed.
123 |
124 | Returns:
125 | NumPy array representing an RGB image with mask applied.
126 | """
127 | t = Time()
128 | result = rgb * np.dstack([mask, mask, mask])
129 | np_info(result, "Mask RGB", t.elapsed())
130 | return result
131 |
132 |
133 | class Time:
134 | """
135 | Class for displaying elapsed time.
136 | """
137 |
138 | def __init__(self):
139 | self.start = datetime.datetime.now()
140 |
141 | def elapsed_display(self):
142 | time_elapsed = self.elapsed()
143 | print("Time elapsed: " + str(time_elapsed))
144 |
145 | def elapsed(self):
146 | self.end = datetime.datetime.now()
147 | time_elapsed = self.end - self.start
148 | return time_elapsed
149 |
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/_layouts/default.html:
--------------------------------------------------------------------------------
1 | {{ content }}
2 |
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/127-rgb-after-filters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/127-rgb-after-filters.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/127-rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/127-rgb.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch-2.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch-avoid-overmask-rem-small-obj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch-avoid-overmask-rem-small-obj.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch-overmask-rem-small-obj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch-overmask-rem-small-obj.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-avoid-overmask-green-ch.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-overmask-green-ch-avoid-overmask-rem-small-obj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-overmask-green-ch-avoid-overmask-rem-small-obj.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-overmask-green-ch-overmask-rem-small-obj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-overmask-green-ch-overmask-rem-small-obj.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-overmask-green-ch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-overmask-green-ch.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/21-rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/21-rgb.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-001.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-002.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-003.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-004.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-005.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-006.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-006.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-007.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-007.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/337-008.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/337-008.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/424-rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/424-rgb.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/498-rgb-after-filters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/498-rgb-after-filters.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/498-rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/498-rgb.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/5-steps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/5-steps.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/TUPAC-TR-002-tile-r34-c34-x33793-y33799-w1024-h1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/TUPAC-TR-002-tile-r34-c34-x33793-y33799-w1024-h1024.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/TUPAC-TR-002-tile-r35-c37-x36865-y34823-w1024-h1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/TUPAC-TR-002-tile-r35-c37-x36865-y34823-w1024-h1024.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/adaptive-equalization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/adaptive-equalization.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/basic-threshold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/basic-threshold.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-closing-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-closing-20.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-closing-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-closing-5.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-dilation-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-dilation-20.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-dilation-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-dilation-5.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-erosion-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-erosion-20.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-erosion-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-erosion-5.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-erosion-no-grays.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-erosion-no-grays.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-erosion-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-erosion-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-opening-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-opening-20.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/binary-opening-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/binary-opening-5.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/blue-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/blue-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/blue-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/blue-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/blue-pen-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/blue-pen-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/blue-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/blue-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/blue.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/canny-original-cropped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/canny-original-cropped.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/canny-original-with-inverse-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/canny-original-with-inverse-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/canny-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/canny-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/canny.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/canny.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/color-histograms-large-tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/color-histograms-large-tile.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/color-histograms-small-tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/color-histograms-small-tile.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pen-filters-no-blue-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pen-filters-no-blue-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pen-filters-no-green-pen-no-blue-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pen-filters-no-green-pen-no-blue-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pen-filters-no-green-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pen-filters-no-green-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pen-filters-original-with-no-green-pen-no-blue-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pen-filters-original-with-no-green-pen-no-blue-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pen-filters-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pen-filters-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pens-background-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pens-background-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pens-background-original-with-inverse-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pens-background-original-with-inverse-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pens-background-original-with-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pens-background-original-with-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/combine-pens-background-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/combine-pens-background-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/complement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/complement.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/contrast-stretching.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/contrast-stretching.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/display-image-with-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/display-image-with-text.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/distribution-of-svs-image-sizes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/distribution-of-svs-image-sizes.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/entropy-grayscale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/entropy-grayscale.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/entropy-original-entropy-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/entropy-original-entropy-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/entropy-original-inverse-entropy-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/entropy-original-inverse-entropy-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/entropy-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/entropy-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/entropy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/entropy.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/eosin-channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/eosin-channel.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/fill-holes-remove-small-holes-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/fill-holes-remove-small-holes-100.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/fill-holes-remove-small-holes-10000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/fill-holes-remove-small-holes-10000.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/fill-holes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/fill-holes.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/filter-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/filter-example.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/filters-001-008.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/filters-001-008.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/grays-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/grays-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/grayscale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/grayscale.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/green-channel-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/green-channel-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/green-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/green-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/green-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/green-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/green-pen-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/green-pen-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/green-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/green-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/green.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/hematoxylin-channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/hematoxylin-channel.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/histogram-equalization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/histogram-equalization.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/hsv-hue-histogram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/hsv-hue-histogram.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/hysteresis-threshold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/hysteresis-threshold.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/kmeans-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/kmeans-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/kmeans-segmentation-after-otsu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/kmeans-segmentation-after-otsu.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/kmeans-segmentation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/kmeans-segmentation.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/not-blue-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/not-blue-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/not-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/not-blue.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/not-green-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/not-green-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/not-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/not-green.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/not-red-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/not-red-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/not-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/not-red.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/openslide-available-slides.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/openslide-available-slides.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/openslide-whole-slide-image-zoomed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/openslide-whole-slide-image-zoomed.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/openslide-whole-slide-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/openslide-whole-slide-image.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/optional-tile-labels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/optional-tile-labels.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/otsu-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/otsu-mask.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/otsu-threshold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/otsu-threshold.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/pink-and-purple-slide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/pink-and-purple-slide.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/purple-slide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/purple-slide.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/rag-thresh-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/rag-thresh-1.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/rag-thresh-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/rag-thresh-20.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/rag-thresh-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/rag-thresh-9.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/rag-thresh-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/rag-thresh-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/red-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/red-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/red-pen-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/red-pen-filter.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/red-pen-slides-filters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/red-pen-slides-filters.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/red-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/red-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/red.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/remove-more-green-more-gray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/remove-more-green-more-gray.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/remove-small-holes-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/remove-small-holes-100.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/remove-small-holes-10000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/remove-small-holes-10000.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/remove-small-objects-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/remove-small-objects-100.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/remove-small-objects-10000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/remove-small-objects-10000.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/scoring-formula.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/scoring-formula.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-rgb-hsv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-rgb-hsv.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-row-25-col-30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-row-25-col-30.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-row-25-col-31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-row-25-col-31.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-row-25-col-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-row-25-col-32.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-tile-tissue-heatmap-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-tile-tissue-heatmap-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-tile-tissue-heatmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-tile-tissue-heatmap.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-tissue-percentage-tile-1000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-tissue-percentage-tile-1000.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-tissue-percentage-tile-1500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-tissue-percentage-tile-1500.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-top-tile-borders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-top-tile-borders.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-top-tile-labels-borders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-top-tile-labels-borders.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-top-tile-labels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-top-tile-labels.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-top-tiles-original.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-top-tiles-original.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-2-top-tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-2-top-tiles.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-4-rgb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-4-rgb.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-4-top-tile-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-4-top-tile-1.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-4-top-tile-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-4-top-tile-2.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-pen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-pen.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/slide-scan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/slide-scan.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/svs-image-sizes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/svs-image-sizes.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/tile-data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/tile-data.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/tiles-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/tiles-page.png
--------------------------------------------------------------------------------
/docs/wsi-preprocessing-in-python/images/wsi-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deroneriksson/python-wsi-preprocessing/c82dcafff48718da041ab99b7534ba697db2cc43/docs/wsi-preprocessing-in-python/images/wsi-example.png
--------------------------------------------------------------------------------