├── .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 += " \n" 1019 | html += " \n" 1020 | 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" 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 --------------------------------------------------------------------------------