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 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])