-
Notifications
You must be signed in to change notification settings - Fork 83
/
mask_polygons.py
275 lines (223 loc) · 11.5 KB
/
mask_polygons.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#####################################################################################################################################################################
# xView2 #
# Copyright 2019 Carnegie Mellon University. #
# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO #
# WARRANTIES OF ANY KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, #
# EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, #
# TRADEMARK, OR COPYRIGHT INFRINGEMENT. #
# Released under a MIT (SEI)-style license, please see LICENSE.md or contact [email protected] for full terms. #
# [DISTRIBUTION STATEMENT A] This material has been approved for public release and unlimited distribution. Please see Copyright notice for non-US Government use #
# and distribution. #
# This Software includes and/or makes use of the following Third-Party Software subject to its own license: #
# 1. SpaceNet (https://github.com/motokimura/spacenet_building_detection/blob/master/LICENSE) Copyright 2017 Motoki Kimura. #
# DM19-0988 #
#####################################################################################################################################################################
import json
from os import path, walk, makedirs
from sys import exit, stderr
from cv2 import fillPoly, imwrite
import numpy as np
from shapely import wkt
from shapely.geometry import mapping, Polygon
from skimage.io import imread
from tqdm import tqdm
import imantics
# This removes the massive amount of scikit warnings of "low contrast images"
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
def get_dimensions(file_path):
"""
:param file_path: The path of the file
:return: returns (width,height,channels)
"""
# Open the image we are going to mask
pil_img = imread(file_path)
img = np.array(pil_img)
w, h, c = img.shape
return (w, h, c)
def mask_polygons_separately(size, shapes):
"""
:param size: A tuple of the (width,height,channels)
:param shapes: A list of points in the polygon from get_feature_info
:returns: a dict of masked polygons with the shapes filled in from cv2.fillPoly
"""
# For each WKT polygon, read the WKT format and fill the polygon as an image
masked_polys = {}
for u in shapes:
sh = shapes[u]
mask_img = np.zeros(size, np.uint8)
i = fillPoly(mask_img, [sh], (255, 255, 255))
masked_polys[u] = i
return masked_polys
def mask_polygons_together(size, shapes):
"""
:param size: A tuple of the (width,height,channels)
:param shapes: A list of points in the polygon from get_feature_info
:returns: A numpy array with the polygons filled 255s where there's a building and 0 where not
"""
# For each WKT polygon, read the WKT format and fill the polygon as an image
mask_img = np.zeros(size, np.uint8)
for u in shapes:
blank = np.zeros(size, np.uint8)
poly = shapes[u]
fillPoly(blank, [poly], (1, 1, 1))
mask_img += blank
# Here we are taking the overlap (+=) and squashing it back to 0
mask_img[mask_img > 1] = 0
# Finally we are taking all 1s and making it pure white (255)
mask_img[mask_img == 1] = 255
return mask_img
def mask_polygons_together_with_border(size, shapes, border):
"""
:param size: A tuple of the (width,height,channels)
:param shapes: A list of points in the polygon from get_feature_info
:returns: a dict of masked polygons with the shapes filled in from cv2.fillPoly
"""
# For each WKT polygon, read the WKT format and fill the polygon as an image
mask_img = np.zeros(size, np.uint8)
for u in shapes:
blank = np.zeros(size, np.uint8)
# Each polygon stored in shapes is a np.ndarray
poly = shapes[u]
# Creating a shapely polygon object out of the numpy array
polygon = Polygon(poly)
# Getting the center points from the polygon and the polygon points
(poly_center_x, poly_center_y) = polygon.centroid.coords[0]
polygon_points = polygon.exterior.coords
# Setting a new polygon with each X,Y manipulated based off the center point
shrunk_polygon = []
for (x,y) in polygon_points:
if x < poly_center_x:
x += border
elif x > poly_center_x:
x -= border
if y < poly_center_y:
y += border
elif y > poly_center_y:
y -= border
shrunk_polygon.append([x,y])
# Transforming the polygon back to a np.ndarray
ns_poly = np.array(shrunk_polygon, np.int32)
# Filling the shrunken polygon to add a border between close polygons
fillPoly(blank, [ns_poly], (1, 1, 1))
mask_img += blank
mask_img[mask_img > 1] = 0
mask_img[mask_img == 1] = 255
return mask_img
def save_masks(masks, output_path, mask_file_name):
"""
:param masks: dictionary of UID:masked polygons from mask_polygons_separately()
:param output_path: path to save the masks
:param mask_file_name: the file name the masks should have
"""
# For each filled polygon, write out a separate file, increasing the name
for m in masks:
final_out = path.join(output_path,
mask_file_name + '_{}.png'.format(m))
imwrite(final_out, masks[m])
def save_one_mask(masks, output_path, mask_file_name):
"""
:param masks: list of masked polygons from the mask_polygons_separately function
:param output_path: path to save the masks
:param mask_file_name: the file name the masks should have
"""
# For each filled polygon, write the mask shape out to the file per image
mask_file_name = path.join(output_path, mask_file_name + '.png')
imwrite(mask_file_name, masks)
def read_json(json_path):
"""
:param json_path: path to load json from
:returns: a python dictionary of json features
"""
annotations = json.load(open(json_path))
return annotations
def get_feature_info(feature):
"""
:param feature: a python dictionary of json labels
:returns: a list mapping of polygons contained in the image
"""
# Getting each polygon points from the json file and adding it to a dictionary of uid:polygons
props = {}
for feat in feature['features']['xy']:
feat_shape = wkt.loads(feat['wkt'])
coords = list(mapping(feat_shape)['coordinates'][0])
props[feat['properties']['uid']] = (np.array(coords, np.int32))
return props
def mask_chips(json_path, images_directory, output_directory, single_file, border):
"""
:param json_path: path to find multiple json files for the chips
:param images_directory: path to the directory containing the images to be masked
:param output_directory: path to the directory where masks are to be saved
:param single_file: a boolean value to see if masks should be saved a single file or multiple
"""
# For each feature in the json we will create a separate mask
# Getting all files in the directory provided for jsons
jsons = [j for j in next(walk(json_path))[2] if '_pre' in j]
# After removing non-json items in dir (if any)
for j in tqdm([j for j in jsons if j.endswith('json')],
unit='poly',
leave=False):
# Our chips start off in life as PNGs
chip_image_id = path.splitext(j)[0] + '.png'
mask_file = path.splitext(j)[0]
# Loading the per chip json
j_full_path = path.join(json_path, j)
chip_json = read_json(j_full_path)
# Getting the full chip path, and loading the size dimensions
chip_file = path.join(images_directory, chip_image_id)
chip_size = get_dimensions(chip_file)
# Reading in the polygons from the json file
polys = get_feature_info(chip_json)
# Getting a list of the polygons and saving masks as separate or single image files
if len(polys) > 0:
if single_file:
if border > 0:
masked_polys = mask_polygons_together_with_border(chip_size, polys, border)
else:
masked_polys = mask_polygons_together(chip_size, polys)
save_one_mask(masked_polys, output_directory, mask_file)
else:
masked_polys = mask_polygons_separately(chip_size, polys)
save_masks(masked_polys, output_directory, mask_file)
if __name__ == "__main__":
import argparse
# Parse command line arguments
parser = argparse.ArgumentParser(
description=
"""mask_polygons.py: Takes in xBD dataset and masks polygons in the image\n\n
WARNING: This could lead to hundreds of output images per input\n""")
parser.add_argument('--input',
required=True,
metavar="/path/to/xBD/",
help='Path to parent dataset directory "xBD"')
parser.add_argument('--single-file',
action='store_true',
help='use to save all masked polygon instances to a single file rather than one polygon per mask file')
parser.add_argument('--border',
default=0,
type=int,
metavar="positive integer for pixel border (e.g. 1)",
help='Positive integer used to shrink the polygon by')
args = parser.parse_args()
# Getting the list of the disaster types under the xBD directory
disasters = next(walk(args.input))[1]
for disaster in tqdm(disasters, desc='Masking', unit='disaster'):
# Create the full path to the images, labels, and mask output directories
image_dir = path.join(args.input, disaster, 'images')
json_dir = path.join(args.input, disaster, 'labels')
output_dir = path.join(args.input, disaster, 'masks')
if not path.isdir(image_dir):
print(
"Error, could not find image files in {}.\n\n"
.format(image_dir),
file=stderr)
exit(2)
if not path.isdir(json_dir):
print(
"Error, could not find labels in {}.\n\n"
.format(json_dir),
file=stderr)
exit(3)
if not path.isdir(output_dir):
makedirs(output_dir)
mask_chips(json_dir, image_dir, output_dir, args.single_file, args.border)