├── README.md ├── make_pyramid_tif.py ├── submission_compress.py ├── tissue_mask.py ├── utils.py └── xml_to_mask.py /README.md: -------------------------------------------------------------------------------- 1 | # PAIP2019: Liver Cancer Segmentation 2 | 3 | ### Task 1: Liver Cancer Segmentation 4 | ### Task 2: Viable Tumor Burden Estimation 5 | ### This competition is part of the MICCAI 2019 Grand Challenge for Pathology. 6 | *** 7 | ### "utils.py" has been released for the participants who raised issues 8 | 9 | It is a sample code for 10 | 1) loading masks 11 | 2) generating an overlay between an original image and a mask 12 | 3) resizing to make an image fit into simple viewers (e.g. Windows default viewer, Fiji, etc.) 13 | 14 | Please note that this code is not providing svs loading part because there are several well-known open source library for this. (e.g. openslide, pyvips, etc.) 15 | 16 | *** 17 | 18 | ### "submission_compress.py" has been released for avoiding logistic issues in Grand-challenge platform 19 | ##### (If you don't compress each tif, the submission system may fail to score your results.) 20 | 21 | It is another extremely simple code for 22 | 1) loading a mask 23 | 2) saving after applying ADOBE_DEFLATE level 9 compression with the same filename (may overwrite) 24 | 3) the output will have around ten times smaller file size compared to the original uncompressed tif 25 | 26 | -------------------------------------------------------------------------------- /make_pyramid_tif.py: -------------------------------------------------------------------------------- 1 | import skimage.io as io 2 | import numpy as np 3 | import pyvips 4 | 5 | # https://github.com/libvips/pyvips/blob/master/examples/pil-numpy-pyvips.py 6 | # map vips formats to np dtypes 7 | format_to_dtype = { 8 | 'uchar': np.uint8, 9 | 'char': np.int8, 10 | 'ushort': np.uint16, 11 | 'short': np.int16, 12 | 'uint': np.uint32, 13 | 'int': np.int32, 14 | 'float': np.float32, 15 | 'double': np.float64, 16 | 'complex': np.complex64, 17 | 'dpcomplex': np.complex128, 18 | } 19 | 20 | # map np dtypes to vips 21 | dtype_to_format = { 22 | 'uint8': 'uchar', 23 | 'int8': 'char', 24 | 'uint16': 'ushort', 25 | 'int16': 'short', 26 | 'uint32': 'uint', 27 | 'int32': 'int', 28 | 'float32': 'float', 29 | 'float64': 'double', 30 | 'complex64': 'complex', 31 | 'complex128': 'dpcomplex', 32 | } 33 | 34 | # numpy array to vips image 35 | def numpy2vips(a): 36 | height, width, bands = a.shape 37 | linear = a.reshape(width * height * bands) 38 | vi = pyvips.Image.new_from_memory(linear.data, width, height, bands, 39 | dtype_to_format[str(a.dtype)]) 40 | return vi 41 | 42 | # vips image to numpy array 43 | def vips2numpy(vi): 44 | return np.ndarray(buffer=vi.write_to_memory(), 45 | dtype=format_to_dtype[vi.format], 46 | shape=[vi.height, vi.width, vi.bands]) 47 | 48 | 49 | def save_pyramid_tif(save_fn, img, Q=70): 50 | img = numpy2vips(img) 51 | img.tiffsave(save_fn, tile=True, compression='jpeg', Q=Q, bigtiff=True, pyramid=True) 52 | 53 | if __name__ == '__main__': 54 | import argparse, pathlib 55 | parser = argparse.ArgumentParser() 56 | parser.add_argument('wsi_fn', help='target image filename') 57 | parser.add_argument('--dst_fn', help='save filename') 58 | args = parser.parse_args() 59 | if not args.dst_fn: 60 | args.dst_fn = '{}_pyramid.tif'.format(pathlib.Path(args.wsi_fn).name[:-4]) 61 | 62 | try: 63 | print('loading {}..'.format(args.wsi_fn)) 64 | img = vips2numpy(pyvips.Image.new_from_file(args.wsi_fn))[:, :, :3] 65 | except: 66 | try: 67 | img = io.imread(args.wsi_fn)[:, :, 3] 68 | except: 69 | print('cannot open {}'.format(args.wsi_fn)) 70 | exit() 71 | print('saving..') 72 | save_pyramid_tif(args.dst_fn, img) 73 | -------------------------------------------------------------------------------- /submission_compress.py: -------------------------------------------------------------------------------- 1 | from tifffile import imsave 2 | import skimage.io as io 3 | import numpy as np 4 | 5 | # an example filename 6 | submission_filename = 'case_083.tif' 7 | # print(submission_filename) 8 | 9 | # load image: any library you're familiar with 10 | img = io.imread(submission_filename) 11 | img = img.astype(np.uint8) # NOTE: tifffile.imsave would show a 'bilevel'-related error w/o this explicit type casting 12 | 13 | # save with compression: ADOBE_DEFLATE algorithm with level 9 (among 0~9) 14 | # NOTE: the saving filename is the same with the loading filename. In other words, it may overwrite your original image. 15 | imsave(submission_filename, img, compress=9) 16 | -------------------------------------------------------------------------------- /tissue_mask.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import skimage.io as io 3 | from threading import Thread 4 | from skimage.morphology import disk, remove_small_objects, remove_small_holes 5 | from scipy.ndimage import label, binary_fill_holes 6 | from tqdm import tqdm 7 | from skimage.measure import regionprops 8 | import os, glob, warnings, pyvips, tifffile 9 | warnings.filterwarnings('ignore', category=UserWarning) 10 | from xml_to_mask import xml2mask # NOTE 11 | 12 | w_size = 256 13 | stride = 256 14 | 15 | total = w_size**2 16 | min_px = total // 10 17 | rm_min_size = 10 18 | rm_min_hole_size = 10 19 | pad_px = w_size * 2 20 | max_rgb = [235, 210, 235] 21 | threshold_vote = 0 22 | 23 | def extract_coord(coord_str): 24 | y_start = coord_str.split('x')[0] 25 | x_start = coord_str.split('x')[1] 26 | return int(y_start), int(x_start) 27 | 28 | def find_minmax(img): 29 | rps = regionprops(img) 30 | assert(len(rps) == 1) 31 | rp = rps[0] 32 | y_min, x_min, y_max, x_max = rp.bbox 33 | y_min = max(y_min - pad_px, 0) 34 | y_max = min(y_max + pad_px, img.shape[0]) 35 | x_min = max(x_min - pad_px, 0) 36 | x_max = min(x_max + pad_px, img.shape[1]) 37 | return (y_min, x_min, y_max, x_max) 38 | 39 | def unit_threshold_and_amend(single_ch_img, threshold, ret): 40 | temp = single_ch_img < threshold 41 | temp = remove_small_holes(label(temp)[0], area_threshold=rm_min_hole_size)>0 42 | #temp = binary_fill_holes(temp) 43 | temp = remove_small_objects(label(temp)[0], min_size=rm_min_size)>0 44 | ret += temp #myutils.multiply(temp, ret>0) # stacking 45 | 46 | def threshold_and_amend(img, ret): 47 | board = np.zeros(img.shape[:-1], dtype=np.uint8) 48 | threads = [] 49 | for i in range(3): 50 | t = Thread(target=unit_threshold_and_amend, args=(img[:, :, i], max_rgb[i], board)) 51 | threads.append(t) 52 | t.start() 53 | for t in threads: 54 | t.join() 55 | ret += (board>threshold_vote).astype(np.uint8) 56 | 57 | def find_foreground(img): 58 | threshold_tissue = np.zeros(img.shape[:-1], dtype=np.uint8) 59 | #entropy_tissue = np.zeros(img.shape[:-1], dtype=np.uint8) 60 | threads = [] 61 | t = Thread(target=threshold_and_amend, args=(img, threshold_tissue)) 62 | threads.append(t) 63 | t.start() 64 | #t = Thread(target=entropy_and_amend, args=(img, entropy_tissue)) 65 | #threads.append(t) 66 | #t.start() 67 | for t in threads: 68 | t.join() 69 | # threshold_tissue_only_big = remove_small_objects(label(threshold_tissue)[0], min_size=256**2)>0 70 | #tissue = ((threshold_tissue + entropy_tissue) > threshold_vote).astype(np.uint8) 71 | tissue = (threshold_tissue > threshold_vote).astype(np.uint8) 72 | return tissue 73 | # return myutils.multiply(threshold_tissue, entropy_tissue).astype(np.uint8) 74 | #return threshold_tissue 75 | 76 | # https://github.com/libvips/pyvips/blob/master/examples/pil-numpy-pyvips.py 77 | # map vips formats to np dtypes 78 | format_to_dtype = { 79 | 'uchar': np.uint8, 80 | 'char': np.int8, 81 | 'ushort': np.uint16, 82 | 'short': np.int16, 83 | 'uint': np.uint32, 84 | 'int': np.int32, 85 | 'float': np.float32, 86 | 'double': np.float64, 87 | 'complex': np.complex64, 88 | 'dpcomplex': np.complex128, 89 | } 90 | 91 | # vips image to numpy array 92 | def vips2numpy(vi): 93 | return np.ndarray(buffer=vi.write_to_memory(), 94 | dtype=format_to_dtype[vi.format], 95 | shape=[vi.height, vi.width, vi.bands]) 96 | 97 | 98 | if __name__ == '__main__': 99 | import argparse, pathlib 100 | parser = argparse.ArgumentParser() 101 | parser.add_argument('wsi_fn', help='the filename of target WSI') 102 | parser.add_argument('--dst_fn', help='save tif filename') 103 | args = parser.parse_args() 104 | if not args.dst_fn: 105 | args.dst_fn = '{}_tissue.tif'.format(pathlib.Path(args.wsi_fn).name[:-4]) 106 | 107 | if not os.path.isfile(args.wsi_fn): 108 | print('wsi not found:', args.wsi_fn) 109 | exit() 110 | 111 | print('loading {}..'.format(args.wsi_fn)) 112 | img = vips2numpy(pyvips.Image.new_from_file(args.wsi_fn))[:, :, :3] 113 | print('generating tissue mask..') 114 | tissue = (find_foreground(img) * 255).astype(np.uint8) 115 | print('saving tissue mask img..') 116 | tifffile.imsave(args.dst_fn, tissue, compress=9) 117 | print('done!') 118 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import skimage.io as io 3 | from skimage.transform import resize 4 | import numpy as np 5 | import os 6 | 7 | OVERLAY_MASK_RATIO = 0.3 8 | 9 | def mask_loader(fn): 10 | ''' 11 | This is a simplest loader for the given tif mask labels, which are compressed in 'LZW' format for logistic convenience. 12 | Scikit-image library can automatically decompress and load them on your physical memory. 13 | ''' 14 | assert(os.path.isfile(fn)) 15 | mask = io.imread(fn) 16 | print('mask shape:', mask.shape) 17 | return mask 18 | 19 | def gen_overlay(orig_img, msk): 20 | ''' 21 | We don't give a loader for original svs image because there are well-known open source libraries already. 22 | (e.g. openslide, pyvips, etc.) 23 | We assume that original image has [H, W, C(=3)] dimension and mask has [H, W] dimension. 24 | ''' 25 | assert(orig_img.shape[:-1] == msk.shape) 26 | img_dark = (orig_img * (1.0 - OVERLAY_MASK_RATIO)).astype(np.uint8) 27 | gmsk = np.zeros(orig_img.shape, dtype=np.uint8) 28 | gmsk[:, :, 1] += (msk * 255 * OVERLAY_MASK_RATIO).astype(np.uint8) # assign GREEN color for mask labels 29 | img_dark += gmsk 30 | img_dark[img_dark>255] = 255 31 | return img_dark.astype(np.uint8) 32 | 33 | def fit_viewer(img): 34 | assert(img.ndim == 2 or img.ndim == 3) 35 | limit = 2147483647 36 | if img.size > limit: 37 | ratio = np.sqrt(limit/img.size) * 0.99 38 | if img.ndim > 2: 39 | ratio /= img.shape[2] 40 | target_shape = [int(img.shape[0]*ratio), int(img.shape[1]*ratio)] 41 | if img.ndim > 2: 42 | target_shape.append(3) 43 | img_resized = resize(img, target_shape, preserve_range=True) # it may take a lot of memory space 44 | return img_resized 45 | else: 46 | return img 47 | 48 | if __name__ == '__main__': 49 | ''' 50 | It is not guaranteed that this sample code will work seamlessly, 51 | nor optimized for obtaining results as quick as possible. 52 | We hope you just can find some useful tips from here. 53 | 54 | ''' 55 | 56 | # Trying to save a decompressed tif of a mask label. 57 | 58 | mask = mask_loader('a_sample_mask.tif') 59 | io.imsave('a_decompressed_mask.tif', mask) 60 | 61 | ## ================================================= 62 | 63 | # Trying to save an overlay image between original svs image and mask image. 64 | # Please note that you need to load an original image on pysical memory before this task. 65 | 66 | #orig_img = your_loader('filename.svs') 67 | overlay = gen_overlay(orig_img, mask) 68 | io.imsave('a_sample_overlay.tif', overlay) 69 | 70 | ## ================================================= 71 | 72 | # Trying to make an image fit into simple viewers(e.g. Windows default, Fiji, etc.), 73 | # which only can handle a smaller number of elements than 2^31. 74 | # When the size of the original overlay exceeds the capable level, you may need this code for resizing. 75 | 76 | overlay_fit = fit_viewer(overlay) 77 | io.imsave('a_sample_overlay_fit.tif', overlay) 78 | 79 | ## ================================================= 80 | 81 | 82 | -------------------------------------------------------------------------------- /xml_to_mask.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import xml.etree.ElementTree as et 3 | from scipy.ndimage import binary_fill_holes 4 | fill = binary_fill_holes 5 | from scipy.ndimage import label 6 | from threading import Thread 7 | import os, glob, tifffile, openslide 8 | 9 | ''' 10 | Annotations (root) 11 | > Annotation (get 'Id' -> 1: whole tumor // 2: viable tumor) 12 | > Regions 13 | > Region (get 'NegativeROA' -> 0: positive area // 1: inner negative area) 14 | > Vertices 15 | > Vertex (get 'X', 'Y') 16 | ''' 17 | pad = 1024 18 | offset = pad 19 | 20 | def load_svs_shape(fn, level=0): 21 | #print('loading shape of <{}> / level={}..'.format(fn, level)) 22 | imgh = openslide.OpenSlide(fn) 23 | return [imgh.level_dimensions[level][1], imgh.level_dimensions[level][0]] 24 | 25 | 26 | def xml2contour(fn, shape, div=None): 27 | print('reconstructing sparse xml to dense contours..') 28 | board1 = None 29 | board2 = None 30 | board_neg = np.zeros(shape[:2], dtype=np.uint8) 31 | #y_min = np.inf 32 | #x_min = np.inf 33 | #y_max = 0 34 | #x_max = 0 35 | # Annotations >> 36 | e = et.parse(fn).getroot() 37 | e = e.findall('Annotation') 38 | assert(len(e) == 2), len(e) 39 | for ann in e: 40 | board = np.zeros(shape[:2], dtype=np.uint8) 41 | id_num = int(ann.get('Id')) 42 | assert(id_num == 1 or id_num == 2) 43 | regions = ann.findall('Regions') 44 | assert(len(regions) == 1) 45 | rs = regions[0].findall('Region') 46 | for i, r in enumerate(rs): 47 | ylist = [] 48 | xlist = [] 49 | dys = [] 50 | dxs = [] 51 | negative_flag = int(r.get('NegativeROA')) 52 | assert(negative_flag == 0 or negative_flag == 1) 53 | negative_flag = bool(negative_flag) 54 | vs = r.findall('Vertices')[0] 55 | vs = vs.findall('Vertex') 56 | vs.append(vs[0]) # last dot should be linked to the first dot 57 | for v in vs: 58 | y, x = int(v.get('Y').split('.')[0]), int(v.get('X').split('.')[0]) 59 | y, x = y+offset, x+offset 60 | if div is not None: 61 | y //= div 62 | x //= div 63 | if y >= shape[0]: 64 | y = shape[0]-1 65 | elif y < 0: 66 | y = 0 67 | if x >= shape[1]: 68 | x = shape[1]-1 69 | elif x < 0: 70 | x = 0 71 | if not negative_flag and board[y, x] < 1: 72 | #board[y, x] = 2 if negative_flag else 1 73 | board[y, x] = 1 74 | elif negative_flag and board_neg[y, x] < 1: 75 | board_neg[y, x] = 1 76 | #if orderlist and order != 0: # must exclude copied first dot 77 | # assert(orderlist[-1] < order), 'orderlist[-1]: {}, order: {}'.format(orderlist[-1], order) 78 | #orderlist.append(order) 79 | if len(ylist) > 1 and ylist[-1] == y and xlist[-1] == x: 80 | continue 81 | ylist.append(y) 82 | xlist.append(x) 83 | #if not negative_flag: 84 | # if y < y_min: 85 | # y_min = y 86 | # elif y > y_max: 87 | # y_max = y 88 | # if x < x_min: 89 | # x_min = x 90 | # elif x > x_max: 91 | # x_max = x 92 | if len(ylist) <= 1: 93 | continue 94 | #print('y - prev:', y, ylist[-1], 'x - prev:', x, xlist[-1]) 95 | y_prev = ylist[-2] 96 | x_prev = xlist[-2] 97 | dy = y - y_prev 98 | dx = x - x_prev 99 | y_factor = 1 if dy > 0 else -1 100 | x_factor = 1 if dx > 0 else -1 101 | y_cur = y_prev + y_factor 102 | x_cur = x_prev + x_factor 103 | it = 0 104 | 105 | if dx == 0: 106 | if y_factor > 0: 107 | while y_cur <= y: 108 | if not negative_flag and board[y_cur, x_cur] < 1: 109 | #board[y_cur, x_cur] = 2 if negative_flag else 1 110 | board[y_cur, x_cur] = 1 111 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 112 | board_neg[y_cur, x_cur] = 1 113 | y_cur += y_factor 114 | else: 115 | while y_cur >= y: 116 | if not negative_flag and board[y_cur, x_cur] < 1: 117 | #board[y_cur, x_cur] = 2 if negative_flag else 1 118 | board[y_cur, x_cur] = 1 119 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 120 | board_neg[y_cur, x_cur] = 1 121 | y_cur += y_factor 122 | #print(dy, it) 123 | continue 124 | if dy == 0: 125 | if x_factor > 0: 126 | while x_cur <= x: 127 | if not negative_flag and board[y_cur, x_cur] < 1: 128 | #board[y_cur, x_cur] = 2 if negative_flag else 1 129 | board[y_cur, x_cur] = 1 130 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 131 | board_neg[y_cur, x_cur] = 1 132 | x_cur += x_factor 133 | else: 134 | while x_cur >= x: 135 | if not negative_flag and board[y_cur, x_cur] < 1: 136 | #board[y_cur, x_cur] = 2 if negative_flag else 1 137 | board[y_cur, x_cur] = 1 138 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 139 | board_neg[y_cur, x_cur] = 1 140 | x_cur += x_factor 141 | #print(dx, it) 142 | continue 143 | assert(dx != 0) 144 | grad = dy / dx 145 | assert(grad != 0.0) 146 | 147 | if abs(dy) > 1 or abs(dx) > 1: 148 | if abs(grad) > 1.0: # abs(dy) > abs(dx), steep 149 | for px in range(abs(dx)): 150 | if y_factor > 0: 151 | while y_cur <= (y_prev + ((x_cur-x_prev) * grad)): 152 | if not negative_flag and board[y_cur, x_cur] < 1: 153 | #board[y_cur, x_cur] = 2 if negative_flag else 1 154 | board[y_cur, x_cur] = 1 155 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 156 | board_neg[y_cur, x_cur] = 1 157 | y_cur += y_factor 158 | else: 159 | while y_cur >= (y_prev + ((x_cur-x_prev) * grad)): 160 | if not negative_flag and board[y_cur, x_cur] < 1: 161 | #board[y_cur, x_cur] = 2 if negative_flag else 1 162 | board[y_cur, x_cur] = 1 163 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 164 | board_neg[y_cur, x_cur] = 1 165 | y_cur += y_factor 166 | x_cur += x_factor 167 | elif abs(grad) < 1.0: # abs(dy) < abs(dx), gentle 168 | for py in range(abs(dy)): 169 | if x_factor > 0: 170 | while x_cur <= (x_prev + ((y_cur-y_prev) / grad)): 171 | if not negative_flag and board[y_cur, x_cur] < 1: 172 | #board[y_cur, x_cur] = 2 if negative_flag else 1 173 | board[y_cur, x_cur] = 1 174 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 175 | board_neg[y_cur, x_cur] = 1 176 | x_cur += x_factor 177 | else: 178 | while x_cur >= (x_prev + ((y_cur-y_prev) / grad)): 179 | if not negative_flag and board[y_cur, x_cur] < 1: 180 | #board[y_cur, x_cur] = 2 if negative_flag else 1 181 | board[y_cur, x_cur] = 1 182 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 183 | board_neg[y_cur, x_cur] = 1 184 | x_cur += x_factor 185 | y_cur += y_factor 186 | 187 | elif abs(dy) == abs(dx): # 45 deg 188 | if dy + dx != 0: # dy and dx have same sign 189 | for p in range(abs(dy)): 190 | if not negative_flag and board[y_cur, x_cur] < 1: 191 | #board[y_cur, x_cur] = 2 if negative_flag else 1 192 | board[y_cur, x_cur] = 1 193 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 194 | board_neg[y_cur, x_cur] = 1 195 | y_cur += y_factor 196 | x_cur += x_factor 197 | else: # dy and dx have opposite sign 198 | for p in range(abs(dy)): 199 | if not negative_flag and board[y_cur, x_cur] < 1: 200 | #board[y_cur, x_cur] = 2 if negative_flag else 1 201 | board[y_cur, x_cur] = 1 202 | elif negative_flag and board_neg[y_cur, x_cur] < 1: 203 | board_neg[y_cur, x_cur] = 1 204 | y_cur += y_factor 205 | x_cur += x_factor 206 | else: # what? 207 | raise AssertionError 208 | else: 209 | #print('initially contacted case!') 210 | pass 211 | # region copy 212 | if id_num == 1: 213 | board1 = np.copy(board) 214 | elif id_num == 2: 215 | board2 = np.copy(board) 216 | #y_min -= 2 217 | #y_max += 2 218 | #x_min -= 2 219 | #x_max += 2 220 | #board = board[y_min:y_max, x_min:x_max].astype(np.uint8) 221 | target_contour = {} 222 | target_contour[1] = board1 223 | target_contour[2] = board2 224 | target_contour['neg'] = board_neg 225 | #assert(y_min < np.inf and x_min < np.inf) 226 | #bbox = y_min, x_min, y_max, x_max 227 | return target_contour#, bbox 228 | 229 | def fill_wrapper(contour, threshold, board, logical_not=False): 230 | if logical_not: 231 | board += np.logical_not(fill(contour>threshold)).astype(np.uint8) 232 | else: 233 | board += fill(contour>threshold).astype(np.uint8) 234 | 235 | def contour2mask(contour, ret_key, ret_dict): 236 | print('generating target mask..') 237 | #neg_mask = np.logical_not(fill(contour>1).astype(np.uint8)) 238 | #pos_mask = fill(contour>0).astype(np.uint8) 239 | #neg_mask = np.zeros(contour.shape, dtype=np.uint8) 240 | #pos_mask = np.zeros(contour.shape, dtype=np.uint8) 241 | mask = np.zeros(contour.shape, dtype=np.uint8) 242 | threads = [] 243 | #t = Thread(target=fill_wrapper, args=(contour, 1, neg_mask, True)) 244 | #threads.append(t) 245 | #t.start() 246 | t = Thread(target=fill_wrapper, args=(contour, 0, mask, ret_key=='neg')) 247 | threads.append(t) 248 | t.start() 249 | for t in threads: 250 | t.join() 251 | #ret_dict[ret_key] = np.multiply(neg_mask, pos_mask).astype(np.uint8) 252 | ret_dict[ret_key] = mask.astype(np.uint8) 253 | 254 | def xml2mask(fn, shape, div=None): 255 | shape_pad = (shape[0]+pad*2, shape[1]+pad*2) 256 | print('padding on the given shape: {} -> {}'.format(shape, shape_pad)) 257 | contour = xml2contour(fn, shape_pad, div=div) # dict 258 | threads = [] 259 | target_mask = {} 260 | for key in contour.keys(): 261 | t = Thread(target=contour2mask, args=(contour[key], key, target_mask)) 262 | threads.append(t) 263 | t.start() 264 | for t in threads: 265 | t.join() 266 | target_mask[2] = np.multiply(target_mask[2], target_mask['neg']).astype(np.uint8) 267 | print('unpadding: {} -> {}'.format(shape_pad, shape)) 268 | for k in target_mask.keys(): 269 | target_mask[k] = target_mask[k][pad:-pad, pad:-pad] 270 | del target_mask['neg'] 271 | return target_mask # dict 272 | 273 | 274 | if __name__ == '__main__': 275 | import argparse, pathlib 276 | parser = argparse.ArgumentParser() 277 | parser.add_argument('xml_fn', help='the filename of target XML annotation') 278 | parser.add_argument('wsi_fn', help='the filename of reference WSI (for size inference)') 279 | parser.add_argument('--dst_fn', help='save tif filename') 280 | args = parser.parse_args() 281 | if not args.dst_fn: 282 | args.dst_fn = '{}_mask.tif'.format(pathlib.Path(args.xml_fn).name[:-4]) 283 | 284 | if not os.path.isfile(args.xml_fn): 285 | print('xml not found:', args.xml_fn) 286 | exit() 287 | if not os.path.isfile(args.wsi_fn): 288 | print('wsi not found:', args.wsi_fn) 289 | exit() 290 | 291 | src_shape = load_svs_shape(args.wsi_fn, level=0) 292 | # dst_shape = load_svs_shape(args.wsi_fn, level=0) 293 | # print(src_shape, '>>', dst_shape) 294 | mask = xml2mask(args.xml_fn, src_shape) 295 | mask = mask * 255 296 | print('saving mask img..') 297 | tifffile.imsave(args.dst_fn, mask, compress=9) 298 | print('done!') 299 | 300 | --------------------------------------------------------------------------------