easyidp 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easyidp/__init__.py +241 -0
- easyidp/cvtools.py +356 -0
- easyidp/data.py +746 -0
- easyidp/geotiff.py +1719 -0
- easyidp/geotools.py +280 -0
- easyidp/jsonfile.py +579 -0
- easyidp/metashape.py +1999 -0
- easyidp/pix4d.py +1903 -0
- easyidp/pointcloud.py +1298 -0
- easyidp/reconstruct.py +776 -0
- easyidp/roi.py +1184 -0
- easyidp/shp.py +472 -0
- easyidp/visualize.py +349 -0
- easyidp-2.0.0.dist-info/LICENSE +21 -0
- easyidp-2.0.0.dist-info/METADATA +147 -0
- easyidp-2.0.0.dist-info/RECORD +32 -0
- easyidp-2.0.0.dist-info/WHEEL +5 -0
- easyidp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +30 -0
- tests/test_cvtools.py +387 -0
- tests/test_data.py +39 -0
- tests/test_geotiff.py +636 -0
- tests/test_init_class_func.py +135 -0
- tests/test_jsonfile.py +132 -0
- tests/test_metashape.py +852 -0
- tests/test_pathtools.py +31 -0
- tests/test_pix4d.py +297 -0
- tests/test_pointcloud.py +524 -0
- tests/test_reconstruct.py +328 -0
- tests/test_roi.py +349 -0
- tests/test_shp.py +241 -0
- tests/test_visualize.py +117 -0
easyidp/__init__.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
__version__ = "2.0.0"
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import warnings
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
|
|
10
|
+
##############
|
|
11
|
+
# dict tools #
|
|
12
|
+
##############
|
|
13
|
+
|
|
14
|
+
class Container(dict):
|
|
15
|
+
"""Self designed dictionary class to fetch items by id or label
|
|
16
|
+
|
|
17
|
+
Caution
|
|
18
|
+
-------
|
|
19
|
+
This object can not be saved by ``pickle``, it will cause problem when loading [1]_
|
|
20
|
+
|
|
21
|
+
References
|
|
22
|
+
----------
|
|
23
|
+
.. [1] https://stackoverflow.com/questions/4014621/a-python-class-that-acts-like-dict
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, suffix=''):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.id_item = {} # {0: item1, 1: item2}
|
|
28
|
+
self.item_label = {} #{"N1W1": 0, "N1W2": 1}, just index it position
|
|
29
|
+
self._suffix = str(suffix)
|
|
30
|
+
|
|
31
|
+
def __setitem__(self, key, item):
|
|
32
|
+
if isinstance(key, int):
|
|
33
|
+
# default method to set values
|
|
34
|
+
# Container[0] = A, while A.label = "N1W1"
|
|
35
|
+
|
|
36
|
+
if key < len(self.item_label):
|
|
37
|
+
if 'label' in dir(item): # has item.label
|
|
38
|
+
# delete old label
|
|
39
|
+
# e.g. {'empty 0': 0, 'empty 1': 1}
|
|
40
|
+
# 0 -> IMG_0001;
|
|
41
|
+
# {'empty 0': 0, 'empty 1': 1, 'IMG_0001': 0}
|
|
42
|
+
old_key = self.id_item[key].label
|
|
43
|
+
del self.item_label[old_key]
|
|
44
|
+
|
|
45
|
+
# add new label
|
|
46
|
+
self.item_label[item.label] = key
|
|
47
|
+
# change the value of one item
|
|
48
|
+
self.id_item[key] = item
|
|
49
|
+
|
|
50
|
+
elif key == len(self.item_label):
|
|
51
|
+
# add a new item
|
|
52
|
+
self.id_item[key] = item
|
|
53
|
+
if 'label' in dir(item):
|
|
54
|
+
# sometimes two items has the same label
|
|
55
|
+
if item.label in self.item_label.keys():
|
|
56
|
+
raise KeyError(f"The given item's label [{item.label}] already exists -> {self.item_label.keys()}")
|
|
57
|
+
else:
|
|
58
|
+
self.item_label[item.label] = key
|
|
59
|
+
else:
|
|
60
|
+
self.item_label[key] = key
|
|
61
|
+
else:
|
|
62
|
+
raise IndexError(f"Index [{key}] out of range (0, {len(self.item_label)})")
|
|
63
|
+
|
|
64
|
+
elif isinstance(key, str):
|
|
65
|
+
# advanced method to change items
|
|
66
|
+
# Container["N1W1"] = B, here assuemt B.label already == "N1W1"
|
|
67
|
+
|
|
68
|
+
# item already exists
|
|
69
|
+
if key in self.item_label.keys():
|
|
70
|
+
idx = self.item_label[key]
|
|
71
|
+
self.id_item[idx] = item
|
|
72
|
+
else: # add new item
|
|
73
|
+
idx = len(self.id_item)
|
|
74
|
+
self.id_item[idx] = item
|
|
75
|
+
if 'label' in dir(item): # has item.label
|
|
76
|
+
self.item_label[item.label] = idx
|
|
77
|
+
else: # act as common dictionary
|
|
78
|
+
self.item_label[key] = idx
|
|
79
|
+
else:
|
|
80
|
+
raise KeyError(f"Key should be 'int', 'str', not {key}")
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key):
|
|
83
|
+
if isinstance(key, int): # index by photo order
|
|
84
|
+
if key < len(self.item_label):
|
|
85
|
+
return self.id_item[key]
|
|
86
|
+
else:
|
|
87
|
+
raise IndexError(f"Index [{key}] out of range (0, {len(self.item_label)})")
|
|
88
|
+
elif isinstance(key, str): # index by photo name
|
|
89
|
+
if key in self.item_label.keys():
|
|
90
|
+
return self.id_item[self.item_label[key]]
|
|
91
|
+
elif self._suffix in key and os.path.splitext(key)[0] in self.item_label.keys():
|
|
92
|
+
return self.id_item[self.item_label[os.path.splitext(key)[0]]]
|
|
93
|
+
else:
|
|
94
|
+
raise KeyError(f"Can not find key [{key}]")
|
|
95
|
+
elif isinstance(key, slice):
|
|
96
|
+
idx_list = list(self.id_item.keys())[key]
|
|
97
|
+
|
|
98
|
+
out = self.copy()
|
|
99
|
+
out.id_item = {k:v for k, v in self.id_item.items() if k in idx_list}
|
|
100
|
+
out.item_label = {k:v for k, v in self.item_label.items() if v in idx_list}
|
|
101
|
+
|
|
102
|
+
return out
|
|
103
|
+
else:
|
|
104
|
+
raise KeyError(f"Key should be 'int', 'str', 'slice', not {key}")
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return self._btf_print()
|
|
108
|
+
|
|
109
|
+
def __str__(self) -> str:
|
|
110
|
+
return self._btf_print()
|
|
111
|
+
|
|
112
|
+
def _btf_print(self):
|
|
113
|
+
title = f'easyidp.{self.__class__.__name__}'
|
|
114
|
+
key_list = list(self.item_label.keys())
|
|
115
|
+
num = len(key_list)
|
|
116
|
+
out_str = f'<{title}> with {num} items\n'
|
|
117
|
+
|
|
118
|
+
# limit the numpy print out
|
|
119
|
+
default_np_thresh = np.get_printoptions()['threshold']
|
|
120
|
+
default_np_suppress = np.get_printoptions()['suppress']
|
|
121
|
+
np.set_printoptions(threshold=4, suppress=True)
|
|
122
|
+
if num == 0:
|
|
123
|
+
out_str = "<Empty easyidp.Container object>"
|
|
124
|
+
elif num > 5:
|
|
125
|
+
for i, k in enumerate(key_list[:2]):
|
|
126
|
+
out_str += f"[{i}]\t{k}\n"
|
|
127
|
+
out_str += repr(self.id_item[self.item_label[k]])
|
|
128
|
+
out_str += '\n'
|
|
129
|
+
out_str += '...\n'
|
|
130
|
+
for i, k in enumerate(key_list[-2:]):
|
|
131
|
+
out_str += f"[{num-2+i}]\t{k}\n"
|
|
132
|
+
out_str += repr(self.id_item[self.item_label[k]])
|
|
133
|
+
out_str += '\n'
|
|
134
|
+
else:
|
|
135
|
+
for i, k in enumerate(key_list):
|
|
136
|
+
out_str += f"[{i}]\t{k}\n"
|
|
137
|
+
out_str += repr(self.id_item[self.item_label[k]])
|
|
138
|
+
out_str += '\n'
|
|
139
|
+
np.set_printoptions(threshold=default_np_thresh, suppress=default_np_suppress)
|
|
140
|
+
|
|
141
|
+
out_str = out_str[:-1]
|
|
142
|
+
return out_str
|
|
143
|
+
|
|
144
|
+
def __len__(self):
|
|
145
|
+
return len(self.id_item)
|
|
146
|
+
|
|
147
|
+
def __delitem__(self, key):
|
|
148
|
+
if isinstance(key, int):
|
|
149
|
+
k = key
|
|
150
|
+
del self.item_label[self.id_item[key]]
|
|
151
|
+
del self.id_item[key]
|
|
152
|
+
elif isinstance(key, str):
|
|
153
|
+
k = self.item_label[key]
|
|
154
|
+
del self.id_item[self.item_label[key]]
|
|
155
|
+
del self.item_label[key]
|
|
156
|
+
else:
|
|
157
|
+
raise KeyError(f"Key should be 'int', 'str', 'slice', not {key}")
|
|
158
|
+
|
|
159
|
+
# update the id
|
|
160
|
+
# a[5] = a.pop(1)
|
|
161
|
+
# https://stackoverflow.com/questions/4406501/change-the-name-of-a-key-in-dictionary
|
|
162
|
+
id_item_keys = list(self.id_item.keys())
|
|
163
|
+
for idx in id_item_keys:
|
|
164
|
+
# e,g. k = 3, idx in [0, 1, 2, 4, 5]
|
|
165
|
+
if idx > k:
|
|
166
|
+
self.id_item[idx-1] = self.id_item.pop(idx)
|
|
167
|
+
|
|
168
|
+
# e.g. {"N1W1": 0, "N1W2": 1},
|
|
169
|
+
label = _find_key(self.item_label, idx)
|
|
170
|
+
self.item_label[label] = idx - 1
|
|
171
|
+
|
|
172
|
+
def __iter__(self):
|
|
173
|
+
return iter(self.id_item.values())
|
|
174
|
+
|
|
175
|
+
def keys(self):
|
|
176
|
+
return self.item_label.keys()
|
|
177
|
+
|
|
178
|
+
def values(self):
|
|
179
|
+
return self.id_item.values()
|
|
180
|
+
|
|
181
|
+
def items(self):
|
|
182
|
+
out_dict = {}
|
|
183
|
+
for k, idx in self.item_label.items():
|
|
184
|
+
out_dict[k] = self.id_item[idx]
|
|
185
|
+
return out_dict.items()
|
|
186
|
+
|
|
187
|
+
def copy(self):
|
|
188
|
+
return deepcopy(self)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _find_key(mydict, value):
|
|
192
|
+
"""a simple function to using dict value to find key
|
|
193
|
+
e.g.
|
|
194
|
+
>>> mydict = {"a": 233, "b": 456}
|
|
195
|
+
>>> _find_key(mydict, 233)
|
|
196
|
+
"a"
|
|
197
|
+
"""
|
|
198
|
+
key_idx = list(mydict.values()).index(value)
|
|
199
|
+
return list(mydict.keys())[key_idx]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
##############
|
|
203
|
+
# path tools #
|
|
204
|
+
##############
|
|
205
|
+
|
|
206
|
+
def get_full_path(short_path):
|
|
207
|
+
if isinstance(short_path, str):
|
|
208
|
+
return Path(short_path)
|
|
209
|
+
elif isinstance(short_path, Path):
|
|
210
|
+
return short_path
|
|
211
|
+
else:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def parse_relative_path(root_path, relative_path):
|
|
215
|
+
# for metashape frame.zip path use only
|
|
216
|
+
if r"../../" in relative_path:
|
|
217
|
+
frame_path = os.path.dirname(os.path.abspath(root_path))
|
|
218
|
+
merge = os.path.join(frame_path, relative_path)
|
|
219
|
+
return os.path.abspath(merge)
|
|
220
|
+
else:
|
|
221
|
+
warnings.warn(f"Seems it is an absolute path [{relative_path}]")
|
|
222
|
+
return relative_path
|
|
223
|
+
|
|
224
|
+
###############
|
|
225
|
+
# import APIs #
|
|
226
|
+
###############
|
|
227
|
+
|
|
228
|
+
from . import (
|
|
229
|
+
visualize,
|
|
230
|
+
cvtools,
|
|
231
|
+
geotools,
|
|
232
|
+
shp,
|
|
233
|
+
jsonfile,
|
|
234
|
+
data,
|
|
235
|
+
)
|
|
236
|
+
from .reconstruct import ProjectPool
|
|
237
|
+
from .pointcloud import PointCloud
|
|
238
|
+
from .geotiff import GeoTiff
|
|
239
|
+
from .pix4d import Pix4D
|
|
240
|
+
from .metashape import Metashape
|
|
241
|
+
from .roi import ROI
|
easyidp/cvtools.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from skimage.draw import polygon2mask
|
|
3
|
+
from shapely.geometry import MultiPoint, Polygon
|
|
4
|
+
|
|
5
|
+
# ignore the warning of shapely convert coordiante
|
|
6
|
+
import warnings
|
|
7
|
+
warnings.filterwarnings("ignore", message="The array interface is deprecated and will no longer work in Shapely 2.0")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def imarray_crop(imarray, polygon_hv, outside_value=0):
|
|
11
|
+
"""crop a given ndarray image by given polygon pixel positions
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
imarray : ndarray
|
|
16
|
+
| the image data in numpy ndarray
|
|
17
|
+
| if the shape is (height, width), view it as DSM data, the data type should be float.
|
|
18
|
+
| if the shape is (height, width, dimen), view it as RGB DOM data (dimen=3 means RGB and dimen=4 means RGBA).
|
|
19
|
+
| the data type for this case should be either 0-1 float, or 0-255 int.
|
|
20
|
+
|
|
21
|
+
.. caution::
|
|
22
|
+
|
|
23
|
+
Currently, the EasyIDP package does not have the ability to handle multi-spectral image data directly.
|
|
24
|
+
If you really want to use this function to crop multi-spectral image with multiple layers, please send each layer one by one.
|
|
25
|
+
|
|
26
|
+
For example, you have a multi-spectral imarray with 6 bands:
|
|
27
|
+
|
|
28
|
+
.. code-block:: python
|
|
29
|
+
|
|
30
|
+
>>> multi_spect_imarray.shape
|
|
31
|
+
(1028, 800, 6)
|
|
32
|
+
|
|
33
|
+
Then using the following for loops to iteratively process each band
|
|
34
|
+
(please modify it by yourself, can not guarantee it works directly)
|
|
35
|
+
|
|
36
|
+
.. code-block:: python
|
|
37
|
+
|
|
38
|
+
>>> band_container = []
|
|
39
|
+
>>> for i in range(0, 6):
|
|
40
|
+
>>> band = multi_spect_imarray[:,:,i]
|
|
41
|
+
>>> out, offset = idp.cvtools.imarray_crop(band, polygon_hv, outside_value=your_geotiff.header['nodata'])
|
|
42
|
+
>>> band_container.append(out)
|
|
43
|
+
>>> final_out = np.dstack(band_container)
|
|
44
|
+
|
|
45
|
+
polygon_hv : 2D ndarray
|
|
46
|
+
| pixel position of boundary point, the order is (horizontal, vertical)
|
|
47
|
+
|
|
48
|
+
.. caution::
|
|
49
|
+
|
|
50
|
+
it is reverted to the numpy imarray axis.
|
|
51
|
+
horzontal = numpy axis 1, vertical = numpy axis 0.
|
|
52
|
+
|
|
53
|
+
outside_value: int | float
|
|
54
|
+
| specify exact value outside the polgyon, default 0.
|
|
55
|
+
| But for some DSM geotiff, it could be -10000.0, depends on the geotiff meta infomation
|
|
56
|
+
|
|
57
|
+
returns
|
|
58
|
+
-------
|
|
59
|
+
imarray_out : ndarray
|
|
60
|
+
the (m,n,d) ndrray to store pixel info
|
|
61
|
+
roi_top_left_offset : ndarray
|
|
62
|
+
the (h, v) pixel index that represent the polygon bbox left top corner
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
# check if the imarray is correct imarray
|
|
66
|
+
if not isinstance(imarray, np.ndarray) or \
|
|
67
|
+
not (
|
|
68
|
+
np.issubdtype(imarray.dtype, np.integer) \
|
|
69
|
+
or \
|
|
70
|
+
np.issubdtype(imarray.dtype, np.floating)
|
|
71
|
+
):
|
|
72
|
+
raise TypeError(f"The `imarray` only accept numpy ndarray integer and float types")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# check if the polygon_hv is float or int, or in proper shape
|
|
76
|
+
# fix the roi is float cause indexing error: github issue #61
|
|
77
|
+
if not isinstance(polygon_hv, np.ndarray):
|
|
78
|
+
raise TypeError(f"Only the numpy 2d array is accepted, not {type(polygon_hv)}")
|
|
79
|
+
if len(polygon_hv.shape) != 2 or polygon_hv.shape[1] !=2:
|
|
80
|
+
raise AttributeError(f"Only the 2d array (xy) is accepted, expected shape like (n, 2), not current {polygon_hv.shape}")
|
|
81
|
+
|
|
82
|
+
if np.issubdtype(polygon_hv.dtype, np.integer):
|
|
83
|
+
pass
|
|
84
|
+
elif np.issubdtype(polygon_hv.dtype, np.floating):
|
|
85
|
+
polygon_hv = polygon_hv.astype(np.uint32)
|
|
86
|
+
else:
|
|
87
|
+
raise TypeError(f"Only polygon coordinates with [np.interger] and [np.floating]"
|
|
88
|
+
f" are accepted, not dtype('{polygon_hv.dtype}')")
|
|
89
|
+
|
|
90
|
+
# (horizontal, vertical) remember to revert in all the following codes
|
|
91
|
+
roi_top_left_offset = polygon_hv.min(axis=0)
|
|
92
|
+
roi_max = polygon_hv.max(axis=0)
|
|
93
|
+
roi_length = roi_max - roi_top_left_offset
|
|
94
|
+
|
|
95
|
+
roi_rm_offset = polygon_hv - roi_top_left_offset
|
|
96
|
+
# the polygon will generate index outside the image
|
|
97
|
+
# this will cause out of index error in the `poly2mask`
|
|
98
|
+
# so need to find out the point locates on the maximum edge and minus 1
|
|
99
|
+
# >>> a = np.array([217, 468]) # roi_max
|
|
100
|
+
# >>> b # polygon
|
|
101
|
+
# array([[217, 456],
|
|
102
|
+
# [ 30, 468],
|
|
103
|
+
# [ 0, 12],
|
|
104
|
+
# [187, 0],
|
|
105
|
+
# [217, 456]])
|
|
106
|
+
# >>> b[:,0] == a[0]
|
|
107
|
+
# array([ True, False, False, False, True])
|
|
108
|
+
# >>> b[b[:,0] == a[0], 0] -= 1
|
|
109
|
+
# >>> b
|
|
110
|
+
# array([[216, 456],
|
|
111
|
+
# [ 30, 468],
|
|
112
|
+
# [ 0, 12],
|
|
113
|
+
# [187, 0],
|
|
114
|
+
# [216, 456]])
|
|
115
|
+
roi_rm_offset[roi_rm_offset[:,0] == roi_length[0], 0] -= 1
|
|
116
|
+
roi_rm_offset[roi_rm_offset[:,1] == roi_length[1], 1] -= 1
|
|
117
|
+
|
|
118
|
+
# remove (160, 160, 1) such fake 3 dimention
|
|
119
|
+
imarray = np.squeeze(imarray)
|
|
120
|
+
|
|
121
|
+
dim = len(imarray.shape)
|
|
122
|
+
if dim == 2:
|
|
123
|
+
# only has 2 dimensions
|
|
124
|
+
# e.g. DSM 1 band only, other value outside polygon = empty value
|
|
125
|
+
|
|
126
|
+
# here need to reverse
|
|
127
|
+
# imarray.shape -> (h, w), but poly2mask need <- (w, h)
|
|
128
|
+
roi_cropped = imarray[roi_top_left_offset[1]:roi_max[1],
|
|
129
|
+
roi_top_left_offset[0]:roi_max[0]]
|
|
130
|
+
rh = roi_cropped.shape[0]
|
|
131
|
+
rw = roi_cropped.shape[1]
|
|
132
|
+
mask = poly2mask((rw, rh), roi_rm_offset)
|
|
133
|
+
|
|
134
|
+
roi_cropped[~mask] = outside_value
|
|
135
|
+
imarray_out = roi_cropped
|
|
136
|
+
|
|
137
|
+
elif dim == 3:
|
|
138
|
+
# has 3 dimensions
|
|
139
|
+
# e.g. DOM with RGB or RGBA band, other value outside changed alpha layer to 0
|
|
140
|
+
# coordinate xy reverted between easyidp and numpy
|
|
141
|
+
roi_cropped = imarray[roi_top_left_offset[1]:roi_max[1],
|
|
142
|
+
roi_top_left_offset[0]:roi_max[0]]
|
|
143
|
+
|
|
144
|
+
rh = roi_cropped.shape[0]
|
|
145
|
+
rw = roi_cropped.shape[1]
|
|
146
|
+
layer_num = roi_cropped.shape[2]
|
|
147
|
+
|
|
148
|
+
# here need to reverse
|
|
149
|
+
# imarray.shape -> (h, w), but poly2mask need <- (w, h)
|
|
150
|
+
mask = poly2mask((rw, rh), roi_rm_offset)
|
|
151
|
+
|
|
152
|
+
if layer_num == 3:
|
|
153
|
+
# DOM without alpha layer - RGB
|
|
154
|
+
# but easyidp will add masked alpha layer to output.
|
|
155
|
+
|
|
156
|
+
# change mask data type to fit with the image data type
|
|
157
|
+
if np.issubdtype(roi_cropped.dtype, np.integer) and roi_cropped.min() >= 0 and roi_cropped.max() <= 255:
|
|
158
|
+
# the image is 0-255 & int type
|
|
159
|
+
roi_cropped = roi_cropped.astype(np.uint8)
|
|
160
|
+
mask = mask.astype(np.uint8) * 255
|
|
161
|
+
elif np.issubdtype(roi_cropped.dtype, np.floating) and roi_cropped.min() >= 0 and roi_cropped.max() <= 1:
|
|
162
|
+
# the image is 0-1 & float type
|
|
163
|
+
mask = mask.astype(roi_cropped.dtype)
|
|
164
|
+
else:
|
|
165
|
+
raise AttributeError(f"Can not handle RGB imarray ranges ({roi_cropped.min()} - {roi_cropped.max()}) with dtype='{roi_cropped.dtype}', "
|
|
166
|
+
f"expected (0-1) with dtype='float' or (0-255) with dtype='int'")
|
|
167
|
+
|
|
168
|
+
# merge alpha mask with cropped images
|
|
169
|
+
imarray_out = np.concatenate([roi_cropped, mask[:, :, None]], axis=2)
|
|
170
|
+
|
|
171
|
+
elif layer_num == 4:
|
|
172
|
+
# DOM with alpha layer - RGBA
|
|
173
|
+
|
|
174
|
+
# merge orginal mask with polygon_hv mask
|
|
175
|
+
original_mask = roi_cropped[:, :, 3].copy()
|
|
176
|
+
original_mask = original_mask > 0 # change type to bool
|
|
177
|
+
merged_mask = original_mask * mask # bool = bool * bool
|
|
178
|
+
|
|
179
|
+
# change mask data type to fit with the image data type
|
|
180
|
+
if np.issubdtype(roi_cropped.dtype, np.integer) and roi_cropped.min() >= 0 and roi_cropped.max() <= 255:
|
|
181
|
+
# the image is 0-255 & int type
|
|
182
|
+
roi_cropped = roi_cropped.astype(np.uint8)
|
|
183
|
+
merged_mask = merged_mask.astype(np.uint8) * 255
|
|
184
|
+
elif np.issubdtype(roi_cropped.dtype, np.floating) and roi_cropped.min() >= 0 and roi_cropped.max() <= 1:
|
|
185
|
+
# the image is 0-1 & float type
|
|
186
|
+
merged_mask = merged_mask.astype(roi_cropped.dtype)
|
|
187
|
+
else:
|
|
188
|
+
raise AttributeError(f"Can not handle RGB imarray ranges ({roi_cropped.min()} - {roi_cropped.max()}) with dtype='{roi_cropped.dtype}', "
|
|
189
|
+
f"expected (0-1) with dtype='float' or (0-255) with dtype='int'")
|
|
190
|
+
|
|
191
|
+
imarray_out = np.dstack([roi_cropped[:,:, 0:3], merged_mask])
|
|
192
|
+
else:
|
|
193
|
+
raise TypeError(f'Unable to solve the layer/band number {layer_num}, only one band DSM or 3|4 band RGB|RGBA DOM are acceptable')
|
|
194
|
+
else:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"Only image dimention=2 (mxn) or 3(mxnxd) are accepted, not current"
|
|
197
|
+
f"[shape={imarray.shape} dim={dim}], please check whether your ROI "
|
|
198
|
+
f"is smaller than one pixel.")
|
|
199
|
+
|
|
200
|
+
return imarray_out, roi_top_left_offset
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def poly2mask(image_shape, poly_coord, engine="skimage"):
|
|
204
|
+
"""convert vector polygon to raster masks
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
image_shape : tuple with 2 element
|
|
209
|
+
.. caution::
|
|
210
|
+
it is reversed with numpy index order
|
|
211
|
+
|
|
212
|
+
(horizontal, vertical) = (width, height)
|
|
213
|
+
|
|
214
|
+
poly_coord : (n, 2) np.ndarray -> dtype = int or float
|
|
215
|
+
.. caution::
|
|
216
|
+
The xy is reversed with numpy index order
|
|
217
|
+
|
|
218
|
+
(horizontal, vertical) = (width, height)
|
|
219
|
+
|
|
220
|
+
engine : str, default "skimage"
|
|
221
|
+
| "skimage" or "shapely"; the "pillow" has been deprecated;
|
|
222
|
+
| skimage - ``skimage.draw.polygon2mask``, the default method;
|
|
223
|
+
| pillow is slight different than "skimage", deprecated;
|
|
224
|
+
| shapely is almost the same with "skiamge", but effiency is very slow, not recommended.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
mask : numpy.ndarray
|
|
229
|
+
the generated binary mask
|
|
230
|
+
|
|
231
|
+
Notes
|
|
232
|
+
-----
|
|
233
|
+
This code is inspired from [1]_ .
|
|
234
|
+
|
|
235
|
+
And for the poly_coord, if using **shapely** engine, it will following this logic for int and float:
|
|
236
|
+
|
|
237
|
+
If dtype is int -> view coord as pixel index number
|
|
238
|
+
Will + 0.5 to coords (pixel center) as judge point
|
|
239
|
+
if dtype is float -> view coords as real coord
|
|
240
|
+
(0,0) will be the left upper corner of pixel square
|
|
241
|
+
|
|
242
|
+
References
|
|
243
|
+
----------
|
|
244
|
+
.. [1] https://stackoverflow.com/questions/62280398/checking-if-a-point-is-contained-in-a-polygon-multipolygon-for-many-points
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
# check the type of input
|
|
249
|
+
# is ndarray -> is int or float ndarray
|
|
250
|
+
if not isinstance(poly_coord, np.ndarray) or \
|
|
251
|
+
not (
|
|
252
|
+
np.issubdtype(poly_coord.dtype, np.integer) \
|
|
253
|
+
or \
|
|
254
|
+
np.issubdtype(poly_coord.dtype, np.floating)
|
|
255
|
+
):
|
|
256
|
+
raise TypeError(f"The `poly_coord` only accept numpy ndarray integer and float types")
|
|
257
|
+
|
|
258
|
+
if len(poly_coord.shape) != 2 or poly_coord.shape[1] != 2:
|
|
259
|
+
raise AttributeError(f"Only nx2 ndarray are accepted, not {poly_coord.shape}")
|
|
260
|
+
|
|
261
|
+
w, h = image_shape
|
|
262
|
+
|
|
263
|
+
# check whether the poly_coords out of mask boundary
|
|
264
|
+
xmin, ymin = poly_coord.min(axis=0)
|
|
265
|
+
xmax, ymax = poly_coord.max(axis=0)
|
|
266
|
+
|
|
267
|
+
if engine == "shapely" and max(xmax-xmin, ymax-ymin) > 100:
|
|
268
|
+
warnings.warn("Shaply Engine can not handle size over 100 efficiently, convert using pillow engine")
|
|
269
|
+
engine = "skimage"
|
|
270
|
+
|
|
271
|
+
if xmin < 0 or ymin < 0 or xmax >= w or ymax >= h:
|
|
272
|
+
raise ValueError(f"The polygon coords ({xmin}, {ymin}, {xmax}, {ymax}) is out of mask boundary [0, 0, {w}, {h}]")
|
|
273
|
+
|
|
274
|
+
if engine == "shapely":
|
|
275
|
+
mask = _shapely_poly2mask(h, w, poly_coord)
|
|
276
|
+
else: # using pillow -> skimage
|
|
277
|
+
# mask = _pillow_poly2mask(h, w, poly_coord)
|
|
278
|
+
# the coordinate of xy is reversed with skimage
|
|
279
|
+
mask = polygon2mask((w, h), poly_coord).T
|
|
280
|
+
|
|
281
|
+
return mask
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _shapely_poly2mask(h, w, poly_coord):
|
|
285
|
+
mask = np.zeros((h, w), dtype=bool)
|
|
286
|
+
|
|
287
|
+
# use the pixel center as judgement points
|
|
288
|
+
x = np.arange(0, w) + 0.5
|
|
289
|
+
y = np.arange(0, h) + 0.5
|
|
290
|
+
|
|
291
|
+
xx, yy = np.meshgrid(x, y)
|
|
292
|
+
|
|
293
|
+
# get the coordinates of all pixel points
|
|
294
|
+
# it is reversed with numpy index order -> [vertical, horizontal]
|
|
295
|
+
pts = np.array([yy.ravel(), xx.ravel()]).T
|
|
296
|
+
points = MultiPoint(pts)
|
|
297
|
+
|
|
298
|
+
# judge the type of polygon coordinates
|
|
299
|
+
if np.issubdtype(poly_coord.dtype, np.integer):
|
|
300
|
+
# is int type, mainly means it represent
|
|
301
|
+
# the id of int rather than coords xy values
|
|
302
|
+
# -> shift 0.5 as the pixel center
|
|
303
|
+
poly = Polygon(poly_coord + 0.5)
|
|
304
|
+
elif np.issubdtype(poly_coord.dtype, np.floating):
|
|
305
|
+
poly = Polygon(poly_coord)
|
|
306
|
+
|
|
307
|
+
points_in = points.intersection(poly)
|
|
308
|
+
|
|
309
|
+
# here will raise warning when obtain coords from shapely multipoints
|
|
310
|
+
# -0.5 turns points center coords to point id
|
|
311
|
+
# here are point index of "masked" pixels
|
|
312
|
+
idx = (np.array(points_in) - 0.5).astype(int)
|
|
313
|
+
|
|
314
|
+
# turn to masks
|
|
315
|
+
# idx -> (pixel horizontal, pixel vertical)
|
|
316
|
+
# it is reversed with numpy index order -> [vertical, horizontal]
|
|
317
|
+
mask[idx[:,1], idx[:,0]] = True
|
|
318
|
+
|
|
319
|
+
return mask
|
|
320
|
+
|
|
321
|
+
# def _pillow_poly2mask(h, w, poly_coord):
|
|
322
|
+
# # deprecated
|
|
323
|
+
# mask = Image.new('1', (w, h), color=0)
|
|
324
|
+
# draw = ImageDraw.Draw(mask)
|
|
325
|
+
|
|
326
|
+
# xy_pil = [tuple(i) for i in poly_coord]
|
|
327
|
+
|
|
328
|
+
# draw.polygon(xy_pil, fill=1, outline=1)
|
|
329
|
+
|
|
330
|
+
# mask = np.array(mask, dtype=bool)
|
|
331
|
+
|
|
332
|
+
# return mask
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def rgb2gray(rgb):
|
|
336
|
+
"""Transform the RGB image to gray image
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
rgb : mxnx3 ndarray
|
|
341
|
+
The RGB ndarray image need to be converted
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
gray : mxn ndarray
|
|
346
|
+
The output 2D ndarray after convension
|
|
347
|
+
|
|
348
|
+
Notes
|
|
349
|
+
-----
|
|
350
|
+
Using the same formular that matplotlib did [1]_ for the transformation.
|
|
351
|
+
|
|
352
|
+
References
|
|
353
|
+
----------
|
|
354
|
+
.. [1] https://stackoverflow.com/questions/12201577/how-can-i-convert-an-rgb-image-into-grayscale-in-python
|
|
355
|
+
"""
|
|
356
|
+
return np.dot(rgb[...,:3], [0.2989, 0.5870, 0.1140])
|