-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathraster_array_tools.py
More file actions
3743 lines (3255 loc) · 155 KB
/
raster_array_tools.py
File metadata and controls
3743 lines (3255 loc) · 155 KB
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Version 3.1; Erik Husby; Polar Geospatial Center, University of Minnesota; 2019
from __future__ import division
import copy
import operator
import os
import sys
from collections import deque
from itertools import product
from PIL import Image
import cv2
import numpy as np
import scipy
import shapely.geometry
import shapely.ops
from scipy import ndimage as sp_ndimage
from skimage.draw import polygon_perimeter
from skimage import morphology as sk_morphology
from skimage.filters.rank import entropy
from skimage.util import unique_rows
if sys.version_info[0] < 3:
from DecimatePoly import DecimatePoly
from raster_io import *
else:
from lib.DecimatePoly import DecimatePoly
from lib.raster_io import *
_script_dir = os.path.dirname(os.path.realpath(__file__))
if sys.version_info[0] < 3:
_ext_fid = open(os.path.join(_script_dir, 'outline.c'), 'r')
_outline = _ext_fid.read()
_ext_fid.close()
_ext_fid = open(os.path.join(_script_dir, 'outline_every1.c'), 'r')
_outline_every1 = _ext_fid.read()
_ext_fid.close()
else:
_ext_fid = open(os.path.join(_script_dir, 'outline.c'), 'r', encoding='utf-8')
_outline = _ext_fid.read()
_ext_fid.close()
_ext_fid = open(os.path.join(_script_dir, 'outline_every1.c'), 'r', encoding='utf-8')
_outline_every1 = _ext_fid.read()
_ext_fid.close()
from osgeo import gdal
gdal.UseExceptions()
class RasterIOError(Exception):
def __init__(self, msg=""):
super(Exception, self).__init__(msg)
class UnsupportedDataTypeError(Exception):
def __init__(self, msg=""):
super(Exception, self).__init__(msg)
class InvalidArgumentError(Exception):
def __init__(self, msg=""):
super(Exception, self).__init__(msg)
class UnsupportedMethodError(Exception):
def __init__(self, msg=""):
super(Exception, self).__init__(msg)
#######################
# Array Manipulations #
#######################
def getWindow(array, i, j, window_shape=(3, 3), output='array', bounds_check=True):
# TODO: Write docstring.
output_choices = ('array', 'indices')
if output not in output_choices:
raise InvalidArgumentError("`output` must be one of {}, "
"but was {}".format(output_choices, output))
win_nrows, win_ncols = window_shape
if bounds_check:
if win_nrows < 1 or win_ncols < 1:
raise InvalidArgumentError("`window_shape` must be a tuple of two positive ints")
arr_nrows, arr_ncols = array.shape
i_backup = i
j_backup = j
if i < 0:
i = arr_nrows + i
if j < 0:
j = arr_ncols + j
if i >= arr_nrows:
raise InvalidArgumentError("Index `i`={} is outside `array` bounds".format(i_backup))
if j >= arr_ncols:
raise InvalidArgumentError("Index `j`={} is outside `array` bounds".format(j_backup))
win_halfrowsz = (win_nrows-1) / 2
win_halfcolsz = (win_ncols-1) / 2
win_r0 = int(i - np.ceil(win_halfrowsz))
win_r1 = int(i + np.floor(win_halfrowsz) + 1)
win_c0 = int(j - np.ceil(win_halfcolsz))
win_c1 = int(j + np.floor(win_halfcolsz) + 1)
if not bounds_check:
if win_r1 == 0:
win_r1 = None
if win_c1 == 0:
win_c1 = None
return ( array[win_r0:win_r1, win_c0:win_c1] if output == 'array'
else (win_r0, win_r1, win_c0, win_c1))
if win_r0 < 0 or win_r1 > arr_nrows or win_c0 < 0 or win_c1 > arr_ncols:
raise InvalidArgumentError("Window falls outside `array` bounds")
return ( array[win_r0:win_r1, win_c0:win_c1] if output == 'array'
else (win_r0, win_r1, win_c0, win_c1))
def rotate_arrays_if_kernel_has_even_sidelength(array, kernel):
"""
Return 180-degree rotated views into the provided arrays
if `kernel` has an even side length.
Parameters
----------
array : ndarray, 2D
Primary array associated with `kernel`.
kernel : ndarray, 2D
Kernel array.
Returns
-------
array_out, kernel_out, rotation_flag : tuple
Tuple containing views into `array` and `kernel`,
and a flag that is True if the views of these two
arrays have been rotated by 180 degrees.
See Also
--------
fix_array_if_rotation_was_applied
Notes
-----
The sole purpose of this function is to assist other
functions in this array utility suite in their attempts
to mimic the behavior of corresponding MATLAB functions
at the pixel level when dealing with a kernel/structure
that has an even side length.
"""
for s in kernel.shape:
if s % 2 == 0:
return np.rot90(array, 2), np.rot90(kernel, 2), True
return array, kernel, False
def fix_array_if_rotation_was_applied(array, rotation_flag):
"""
Return 180-degree rotated view into the provided array
if `rotation_flag` is True.
Parameters
----------
array : ndarray, 2D
Array that may or may not need undoing of rotation.
rotation_flag : bool
True if `array` rotation should be undone.
False if `array` does not need undoing of rotation.
Returns
-------
array_out : ndarray, 2D
View into `array` that may or may not have had
rotation undone.
See Also
--------
rotate_arrays_if_kernel_has_even_sidelength
Notes
-----
The sole purpose of this function is to assist other
functions in this array utility suite in their attempts
to mimic the behavior of corresponding MATLAB functions
at the pixel level when dealing with a kernel/structure
that has an even side length.
"""
return np.rot90(array, 2) if rotation_flag else array
def rot90_pixcoords(coords, shape_in, k=1):
"""
Rotate 2D (row, col) pixel coordinates taken from an
array of a defined nrows x ncols shape by 90 degrees.
Rotation direction is counterclockwise.
Parameters
----------
coords : 2D ndarray or list/tuple of two 1D ndarrays
2D (row, col) pixel coordinates.
May be in the format of the output of np.where
(2D ndarray, shape like (npoints, 2)) [1] or
np.argwhere (tuple of two 1D ndarrays, each of
size npoints) [2].
shape_in : tuple of positive int
Shape of array that pixel coordinates came from
before the desired rotation has been applied,
like (nrows, ncols) output of `array.shape`.
k : int
Number of times the coordinates are rotated by
90 degrees.
Returns
-------
coords_out : same format, type, shape as `coords`
2D (row, col) pixel coordinates rotated from
the corresponding coordinates in `coords`.
See Also
--------
numpy.rot90 [3]
flip_pixcoords
Notes
-----
Say `coords` index into array 'a' to return values
of a set of pixels 'a_vals' as follows:
`a_vals = a[coords]`
Rotate both `a` and `coords` 90 degrees the same
number of times `k` to get array 'b' and pixel
coords 'coords_b' that index into 'b' to return
'b_vals'.
`b = numpy.rot90(a, k)`
`coords_b = rot90_pixcoords(coords, a.shape, k)`
`b_vals = b[coords_b]`
The values in 'a_vals' and 'b_vals' are identical.
References
----------
.. [1] https://docs.scipy.org/doc/numpy/reference/generated/numpy.where.html
.. [2] https://docs.scipy.org/doc/numpy/reference/generated/numpy.argwhere.html
.. [3] https://docs.scipy.org/doc/numpy/reference/generated/numpy.rot90.html
"""
if type(coords) == np.ndarray:
row_in, col_in = coords.T
else:
row_in, col_in = coords
k = k % 4
if k == 0:
row_out = row_in
col_out = col_in
elif k == 1:
row_out = (shape_in[1]-1) - col_in
col_out = row_in
elif k == 2:
row_out = (shape_in[0]-1) - row_in
col_out = (shape_in[1]-1) - col_in
elif k == 3:
row_out = col_in
col_out = (shape_in[0]-1) - row_in
if type(coords) == np.ndarray:
result = np.array([row_out, col_out]).T
else:
result = (row_out, col_out)
return result
def flip_pixcoords(coords, shape_in, axis=0):
"""
Flip 2D (row, col) pixel coordinates taken from an
array of a defined nrows x ncols shape across an axis.
Parameters
----------
coords : 2D ndarray or list/tuple of two 1D ndarrays
2D (row, col) pixel coordinates.
May be in the format of the output of np.where
(2D ndarray, shape like (npoints, 2)) [1] or
np.argwhere (tuple of two 1D ndarrays, each of
size npoints) [2].
shape_in : tuple of positive int
Shape of array that pixel coordinates came from,
like (nrows, ncols) output of `array.shape`.
axis : 0 or 1
If 0, flip coordinates vertically.
If 1, flip coordinates horizontally.
See Also
--------
numpy.rot90 [3]
rot90_pixcoords
Returns
-------
coords_out : same format, type, shape as `coords`
2D (row, col) pixel coordinates flipped from
the corresponding coordinates in `coords`.
Notes
-----
Say `coords` index into array 'a' to return values
of a set of pixels 'a_vals' as follows:
`a_vals = a[coords]`
Flip both `a` and `coords` over the same axis with
number `axis` to get array 'b' and pixel coords
'coords_b' that index into 'b' to return 'b_vals'.
`b = numpy.flip(a, axis)`
`coords_b = flip_pixcoords(coords, a.shape, axis)`
`b_vals = b[coords_b]`
The values in 'a_vals' and 'b_vals' are identical.
References
----------
.. [1] https://docs.scipy.org/doc/numpy/reference/generated/numpy.where.html
.. [2] https://docs.scipy.org/doc/numpy/reference/generated/numpy.argwhere.html
.. [3] https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html
"""
if type(coords) == np.ndarray:
row_in, col_in = coords.T
else:
row_in, col_in = coords
if axis == 0:
row_out = (shape_in[0]-1) - row_in
col_out = col_in
elif axis == 1:
row_out = row_in
col_out = (shape_in[1]-1) - col_in
else:
raise InvalidArgumentError("`axis` must be 0 or 1")
if type(coords) == np.ndarray:
result = np.array([row_out, col_out]).T
else:
result = (row_out, col_out)
return result
def array_round_proper(array, in_place=False):
"""
Round data in a floating point array to the nearest integer,
rounding up for positive X.5 and down for negative X.5.
Parameters
----------
array : ndarray of floating dtype
Floating point array to round.
in_place : bool
If True, round array in place.
If False, copy array before rounding.
Returns
-------
array_round : ndarray of floating dtype
The rounded array.
"""
if not in_place:
array = np.copy(array)
array_gt_zero = array > 0
np.add(array, 0.5, out=array, where=array_gt_zero)
np.floor(array, out=array, where=array_gt_zero)
del array_gt_zero
array_lt_zero = array < 0
np.subtract(array, 0.5, out=array, where=array_lt_zero)
np.ceil(array, out=array, where=array_lt_zero)
del array_lt_zero
return array
def astype_round_and_crop(array, dtype_out, allow_modify_array=False):
"""
Cast a floating point array to an integer data type,
first rounding data values and cropping all values to
the minimum and maximum representable values for the
output data type.
Parameters
----------
array : ndarray
Array containing data to be cast.
dtype_out : numpy data type (e.g. numpy.int32) or numpy.dtype
The data type `array` is to be cast to.
allow_modify_array : bool
If True, values in input `array` may be modified.
Returns
-------
array_out : ndarray of type `dtype_out`
The new array that has been cast from `array`.
Notes
-----
This function is meant to replicate MATLAB array type casting.
"""
# The trivial case
if dtype_out == bool:
return array.astype(dtype_out)
array_dtype_np = array.dtype.type
dtype_out_np = dtype_out if type(dtype_out) != np.dtype else dtype_out.type
if np.issubdtype(array_dtype_np, np.floating) and np.issubdtype(dtype_out_np, np.integer):
# TODO: Consider replacing the following potentially costly call with
# -t np.around(array) if round-half-to-nearest-whole-even is acceptable.
array = array_round_proper(array, allow_modify_array)
return astype_cropped(array, dtype_out_np, allow_modify_array)
def astype_cropped(array, dtype_out, allow_modify_array=False):
"""
Cast an array to a new data type, first cropping all values
to the minimum and maximum representable values for the
output data type.
Parameters
----------
array : ndarray
Array containing data to be cast.
dtype_out : numpy data type (e.g. numpy.int32) or numpy.dtype
The data type `array` is to be cast to.
allow_modify_array : bool
If True, values in input `array` may be modified.
Returns
-------
array_cropped : ndarray of type `dtype_out`
The new array that has been cast from `array`.
Notes
-----
The purpose of this function is to prevent underflow and
underflow during casting, something numpy.ndarray.astype
does not do. [1]
References
----------
.. [1] https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.astype.html
"""
# The trivial case
if dtype_out == bool:
return array.astype(dtype_out)
dtype_out_np = dtype_out if type(dtype_out) != np.dtype else dtype_out.type
dtype_info_fn = np.finfo if np.issubdtype(dtype_out_np, np.floating) else np.iinfo
dtype_out_min = dtype_info_fn(dtype_out_np).min
dtype_out_max = dtype_info_fn(dtype_out_np).max
array_cropped = array if allow_modify_array else None
try:
array_cropped = np.clip(array, dtype_out_min, dtype_out_max, out=array_cropped)
except OverflowError:
dtype_out_min_float = float(dtype_out_min)
dtype_out_max_float = float(dtype_out_max)
warn("Integers for {} clip range [{}, {}] are too large for underlying C code of numpy.clip(). "
"Casting clip range to float: [{}, {}]".format(dtype_out_np(1).dtype,
dtype_out_min, dtype_out_max,
dtype_out_min_float, dtype_out_max_float))
array_cropped = np.clip(array, dtype_out_min_float, dtype_out_max_float, out=array_cropped)
return array_cropped.astype(dtype_out)
def getDataArray(array, label=0, label_type='nodata'):
"""
Classify values in an array as "data" or non-"data".
Parameters
----------
array : ndarray
Array to be classified.
label : bool/int/float
Value of nodes in `array` that are classified as
"data" (if label_type='data')
or non-"data" (if label_type='nodata').
label_type : str; 'data' or 'nodata'
Whether `label` is a classification for "data"
or non-"data" nodes.
Returns
-------
data_array : ndarray of bool, same shape as `array`
Binary mask of `array` where "data" nodes are one
and non-"data" nodes are zero.
"""
label_type_choices = ('data', 'nodata')
if label_type not in label_type_choices:
raise InvalidArgumentError("`label_type` must be one of {}, "
"but was {}".format(label_type_choices, label_type))
if (array.dtype == bool
and ((label_type == 'nodata' and label == 0)
or (label_type == 'data' and label == 1))):
data_array = array
elif np.isnan(label):
data_array = np.isnan(array) if label_type == 'data' else ~np.isnan(array)
else:
data_array = (array == label) if label_type == 'data' else (array != label)
return data_array
######################
# Array Calculations #
######################
def interp2_fill_oob(X, Y, Zi, Xi, Yi, fillval=np.nan, coord_grace=True):
# Rows and columns of Zi outside the domain of Z are made NaN.
# Assume X and Y coordinates are monotonically increasing/decreasing
# so hopefully we only need to work a short way inwards from the edges.
Xi_size = Xi.size
Yi_size = Yi.size
Xmin = min(X[0], X[-1])
Ymax = max(Y[0], Y[-1])
x_lfttest_val = X[0]
x_rgttest_val = X[-1]
y_toptest_val = Y[0]
y_bottest_val = Y[-1]
if x_lfttest_val == Xmin:
# X-coords increase from left to right.
x_lfttest_op = operator.lt
x_rgttest_op = operator.gt
else:
# X-coords decrease from left to right.
x_lfttest_op = operator.gt
x_rgttest_op = operator.lt
if y_toptest_val == Ymax:
# Y-coords decrease from top to bottom.
y_toptest_op = operator.gt
y_bottest_op = operator.lt
else:
# Y-coords increase from top to bottom.
y_toptest_op = operator.lt
y_bottest_op = operator.gt
if coord_grace:
x_grace = (X[1] - X[0]) / 64
y_grace = (Y[1] - Y[0]) / 16
x_lfttest_val -= x_grace
x_rgttest_val += x_grace
y_toptest_val -= y_grace
y_bottest_val += y_grace
x_lfttest_op = operator.le if x_lfttest_op(0, 1) else operator.ge
x_rgttest_op = operator.le if x_rgttest_op(0, 1) else operator.ge
y_toptest_op = operator.le if y_toptest_op(0, 1) else operator.ge
y_bottest_op = operator.le if y_bottest_op(0, 1) else operator.ge
i = 0
while x_lfttest_op(Xi[i], x_lfttest_val) and i < Xi_size:
Zi[:, i] = fillval
i += 1
i = -1
while x_rgttest_op(Xi[i], x_rgttest_val) and i >= -Xi_size:
Zi[:, i] = fillval
i -= 1
j = 0
while y_toptest_op(Yi[j], y_toptest_val) and j < Yi_size:
Zi[j, :] = fillval
j += 1
j = -1
while y_bottest_op(Yi[j], y_bottest_val) and j >= -Yi_size:
Zi[j, :] = fillval
j -= 1
return Zi
# def interp2_cv2(X, Y, Z, Xi, Yi, interp_str, extrapolate=False, oob_val=np.nan):
# xx = np.repeat(np.reshape((Xi-X[0]/2, (1, X.size)), Y.size, axis=0)
# yy = np.repeat(np.reshape((Yi-Y[0])/-2, (Y.size, 1)), X.size, axis=1)
# cv2.remap(Z, xx.astype(np.float32), yy.astype(np.float32), cv2.INTER_LINEAR)
# pass
def interp2_gdal(X, Y, Z, Xi, Yi, interp_str, extrapolate=False, oob_val=np.nan):
"""
Resample array data from one set of x-y grid coordinates to another.
Parameters
----------
X : ndarray, 1D
Grid coordinates corresponding to all columns in the raster image,
from left to right, such that `X[j]` specifies the x-coordinate for
all pixels in `Z[:, j]`.
Y : ndarray, 1D
Grid coordinates corresponding to all rows in the raster image,
from top to bottom, such that `Y[i]` specifies the y-coordinate for
all pixels in `Z[i, :]`.
Z : ndarray, 2D
Array containing values to be resampled.
Xi : ndarray, 1D
New grid x-coordinates, like `X` array.
Yi : ndarray, 1D
New grid y-coordinates, like `Y` array.
interp_str : str
Interpolation/resampling method, must be one of the following:
'nearest', 'linear', 'cubic', 'spline', 'lanczos', 'average', 'mode'
extrapolate : bool
Whether or not to interpolate values for pixels with new grid coords
`Xi` and `Yi` that fall outside the range of old grid coords `X` and `Y`.
If True, allow the interpolation method to set the values of these pixels.
If False, set the values of these pixels to `oob_val`.
oob_val : int/float
(Option only applies when `extrapolate=True`.)
Value to fill any regions of the output array where new grid coords
`Xi` and `Yi` fall outside the range of old grid coords `X` and `Y`.
Returns
-------
Zi : ndarray, 2D, same shape and type as `Z`
The resampled array.
"""
dtype_gdal, promote_dtype = dtype_np2gdal(Z.dtype)
if promote_dtype is not None:
Z = Z.astype(promote_dtype)
interp_gdal = interp_str2gdal(interp_str)
mem_drv = gdal.GetDriverByName('MEM')
ds_in = mem_drv.Create('', X.size, Y.size, 1, dtype_gdal)
ds_in.SetGeoTransform((X[0], X[1]-X[0], 0,
Y[0], 0, Y[1]-Y[0]))
ds_in.GetRasterBand(1).WriteArray(Z)
ds_out = mem_drv.Create('', Xi.size, Yi.size, 1, dtype_gdal)
ds_out.SetGeoTransform((Xi[0], Xi[1]-Xi[0], 0,
Yi[0], 0, Yi[1]-Yi[0]))
gdal.ReprojectImage(ds_in, ds_out, '', '', interp_gdal)
Zi = ds_out.GetRasterBand(1).ReadAsArray()
if not extrapolate:
interp2_fill_oob(X, Y, Zi, Xi, Yi, oob_val)
return Zi
def interp2_scipy(X, Y, Z, Xi, Yi, interp, extrapolate=False, oob_val=np.nan,
griddata=False,
SBS=False,
RGI=False, RGI_extrap=True, RGI_fillVal=None,
CLT=False, CLT_fillVal=np.nan,
RBS=False):
# TODO: Rewrite docstring in new standard.
"""
Aims to provide similar functionality to interp2_gdal using SciPy's
interpolation library. However, initial tests show that interp2_gdal
both runs more quickly and produces output more similar to MATLAB's
interp2 function for every method required by Ian's mosaicking script.
griddata, SBS, and CLT interpolation methods are not meant to be used
for the resampling of a large grid as is done here.
"""
order_dict = {
'nearest' : 0,
'linear' : 1,
'bilinear' : 1,
'quadratic': 2,
'cubic' : 3,
'bicubic' : 3,
'quartic' : 4,
'quintic' : 5,
}
order = order_dict[interp]
method_set = True in (griddata, SBS, RGI, CLT, RBS)
if griddata:
# Supports nearest, linear, and cubic interpolation methods.
# Has errored out with "QH7074 qhull warning: more than 16777215 ridges.
# ID field overflows and two ridges may have the same identifier."
# when used on large arrays. Fails to draw a convex hull of input points.
# Needs more testing, but seems to handle NaN input. Output for linear and
# cubic methods shows NaN borders when interpolating out of input domain.
xx, yy = np.meshgrid(X, Y)
xxi, yyi = np.meshgrid(Xi, Yi)
Zi = scipy.interpolate.griddata((xx.flatten(), yy.flatten()), Z.flatten(),
(xxi.flatten(), yyi.flatten()), interp)
Zi.resize((Yi.size, Xi.size))
elif SBS:
# Supports all 5 orders of spline interpolation.
# Can't handle NaN input; results in all NaN output.
xx, yy = np.meshgrid(X, Y)
xxi, yyi = np.meshgrid(Xi, Yi)
fn = scipy.interpolate.SmoothBivariateSpline(xx.flatten(), yy.flatten(), Z.flatten(),
kx=order, ky=order)
Zi = fn.ev(xxi, yyi)
Zi.resize((Yi.size, Xi.size))
elif RGI or (not method_set and (order == 0 or (order == 1 and np.any(np.isnan(Z))))):
# Supports nearest and linear interpolation methods.
xxi, yyi = np.meshgrid(Xi, Yi[::-1])
pi = np.column_stack((yyi.flatten(), xxi.flatten()))
fn = scipy.interpolate.RegularGridInterpolator((Y[::-1], X), Z, method=interp,
bounds_error=(not RGI_extrap), fill_value=RGI_fillVal)
Zi = fn(pi, method=interp)
Zi.resize((Yi.size, Xi.size))
elif CLT or (not method_set and (order == 3 and np.any(np.isnan(Z)))):
# Performs cubic interpolation of data,
# but includes logic to first perform a nearest resampling of input NaNs.
# Produces the same error as scipy.interpolate.griddata when used on large arrays.
if np.any(np.isnan(Z)):
Zi = interp2_scipy(X, Y, Z, Xi, Yi, 'nearest')
Zi_data = np.where(~np.isnan(Zi))
Z_data = np.where(~np.isnan(Z))
p = np.column_stack((Z_data[0], Z_data[1]))
pi = np.column_stack((Zi_data[0], Zi_data[1]))
fn = scipy.interpolate.CloughTocher2DInterpolator(p, Z[Z_data], fill_value=CLT_fillVal)
Zi[Zi_data] = fn(pi)
else:
xx, yy = np.meshgrid(X, Y)
xxi, yyi = np.meshgrid(Xi, Yi)
p = np.column_stack((xx.flatten(), yy.flatten()))
pi = np.column_stack((xxi.flatten(), yyi.flatten()))
fn = scipy.interpolate.CloughTocher2DInterpolator(p, Z.flatten(), fill_value=CLT_fillVal)
Zi = fn(pi)
Zi.resize((Yi.size, Xi.size))
elif RBS or (not method_set and (order in (2, 4))):
# Supports all 5 orders of spline interpolation.
# Can't handle NaN input; results in all NaN output.
fn = scipy.interpolate.RectBivariateSpline(Y[::-1], X, Z,
kx=order, ky=order)
Zi = fn(Yi[::-1], Xi, grid=True)
else:
# Supports linear, cubic, and quintic interpolation methods.
# Can't handle NaN input; results in all NaN output.
# Default interpolator for its presumed efficiency.
fn = scipy.interpolate.interp2d(X, Y[::-1], Z, kind=interp)
Zi = fn(Xi, Yi)
if not extrapolate:
interp2_fill_oob(X, Y, Zi, Xi, Yi, oob_val)
return Zi
def imresize(array, size, interp='bicubic', dtype_out='input',
method='cv2', float_resize=True, round_proper=True,
one_dim_axis=1):
"""
Resize an array.
Parameters
----------
array : ndarray, 2D
The array to resize.
size : shape tuple (2D) or scalar value
If shape tuple, returns an array of this size.
If scalar value, returns an array of shape
that is `size` times the shape of `array`.
interp : str; 'nearest', 'area', 'bilinear', 'bicubic', or 'lanczos'
Interpolation method to use during resizing.
dtype_out : str; 'input' or 'float'
If 'input', data type of the returned array is
the same as `array`.
If 'float' and `array` data type is of floating type,
data type of the returned array is the same.
If 'float' and `array` data type is of integer type,
data type of the returned array is float32.
method : str; 'cv2', 'pil', 'gdal', or 'scipy'
Specifies which method used to perform resizing.
'cv2' ------ cv2.resize [1]
'pil' ------ PIL.Image.resize [2]
'scipy' ---- scipy.misc.imresize (WILL BE RETIRED SOON) [3]
'gdal' ----- interp2_gdal (local, utilizes gdal.ReprojectImage [4])
float_resize : bool
If True, convert integer arrays to float32 before resizing.
round_proper : bool
If the resized array is converted from floating
to an integer data type (such as when `float_resize=True`
and `dtype_out='input'`)...
- If True, round X.5 values up to (X + 1).
- If False, round X.5 values to nearest even integer to X.
one_dim_axis : int, 0 or 1
Which directional layout to give to a one-dimensional
`array` before resizing.
If 0, array runs vertically downwards across rows.
If 1, array runs horizontally rightwards across columns.
Returns
-------
array_r : ndarray, 2D, same type as `array`
The resized array.
See Also
--------
imresize_pil
imresize_old
Notes
-----
This function is meant to replicate MATLAB's `imresize` function [5].
References
----------
.. [1] https://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html#void resize(InputArray src, OutputArray dst, Size dsize, double fx, double fy, int interpolation)
.. [2] http://pillow.readthedocs.io/en/3.1.x/reference/Image.html
.. [3] https://docs.scipy.org/doc/scipy/reference/generated/scipy.misc.imresize.html
.. [4] http://gdal.org/java/org/gdal/gdal/gdal.html#ReprojectImage-org.gdal.gdal.Dataset-org.gdal.gdal.Dataset-java.lang.String-java.lang.String-int-double-double-org.gdal.gdal.ProgressCallback-java.util.Vector-
https://svn.osgeo.org/gdal/trunk/autotest/alg/reproject.py
.. [5] https://www.mathworks.com/help/images/ref/imresize.html
"""
array_backup = array
dtype_in = array.dtype
method_choices = ('cv2', 'pil', 'scipy', 'gdal')
if method not in method_choices:
raise InvalidArgumentError("`method` must be one of {}, but was '{}'".format(method_choices, method))
dtype_out_choices = ('input', 'float')
if dtype_out not in dtype_out_choices:
raise InvalidArgumentError("`dtype_out` must be one of {}, but was '{}'".format(dtype_out_choices, dtype_out))
# Handle interpolation method lookups.
interp_dict = None
if method == 'cv2':
interp_dict = {
'nearest' : cv2.INTER_NEAREST,
'area' : cv2.INTER_AREA,
'bilinear' : cv2.INTER_LINEAR,
'bicubic' : cv2.INTER_CUBIC,
'lanczos' : cv2.INTER_LANCZOS4,
}
elif method == 'pil':
interp_dict = {
'nearest' : Image.NEAREST,
'box' : Image.BOX,
'linear' : Image.BILINEAR,
'bilinear' : Image.BILINEAR,
'hamming' : Image.HAMMING,
'cubic' : Image.BICUBIC,
'bicubic' : Image.BICUBIC,
'lanczos' : Image.LANCZOS,
}
if interp_dict is not None:
if interp not in interp_dict.keys():
raise UnsupportedMethodError("`interp` must be one of {}, but was '{}'".format(interp_dict.keys(), interp))
interp_code = interp_dict[interp]
# Handle 1D array input.
one_dim_flag = False
if array.ndim == 1:
one_dim_flag = True
if one_dim_axis == 0:
array_shape_1d = (array.size, 1)
elif one_dim_axis == 1:
array_shape_1d = (1, array.size)
else:
raise InvalidArgumentError("`one_dim_axis` must be either 0 or 1")
array = np.reshape(array, array_shape_1d)
# If a resize factor is provided for size, round up the x, y pixel
# sizes for the output array to match MATLAB's imresize function.
new_shape = size if type(size) == tuple else tuple(np.ceil(np.dot(size, array.shape)).astype(int))
if one_dim_flag and type(size) != tuple:
new_shape = (new_shape[0], 1) if one_dim_axis == 0 else (1, new_shape[1])
# The trivial case
if new_shape == array.shape:
return array_backup.copy()
# Handle input data type and conversions.
promote_dtype = None
promote_is_demote = False
if float_resize:
if np.issubdtype(dtype_in, np.floating):
pass
else:
array = array.astype(np.float32)
elif method == 'cv2':
if dtype_in == bool:
promote_dtype = np.uint8
elif dtype_in == np.int8:
promote_dtype = np.int16
elif dtype_in == np.float16:
promote_dtype = np.float32
elif dtype_in in (np.int32, np.uint32, np.int64, np.uint64):
raise InvalidArgumentError("`array` data type cannot be of 32/64-bit int/uint "
"when method='{}', but was {}; consider setting "
"`float_resize=True`".format(method, dtype_in))
elif method == 'pil':
if dtype_in == np.uint16:
promote_dtype = np.int32
elif dtype_in in (np.uint32, np.int64, np.uint64):
if np.any(array > np.iinfo(np.int32).max) or np.any(array < np.iinfo(np.int32).min):
raise InvalidArgumentError("`array` data type ({}) is not supported by method='{}', "
"but values cannot fit in int32; consider setting "
"`float_resize=True`")
promote_dtype = np.int32
promote_is_demote = True
elif dtype_in == np.float16:
promote_dtype = np.float32
if promote_dtype is not None:
warn("`array` data type ({}) is not supported by '{}' resizing method, "
"but can safely be {}{} to {} for processing".format(dtype_in, method,
'promoted'*(not promote_is_demote), 'demoted'*promote_is_demote, promote_dtype(1).dtype))
array = array.astype(promote_dtype)
# Resize array.
if method == 'cv2':
array_r = cv2.resize(array, tuple(list(new_shape)[::-1]), interpolation=interp_code)
elif method == 'pil':
image = (Image.frombytes(mode='1', size=array.shape[::-1], data=np.packbits(array, axis=1))
if array.dtype == bool else Image.fromarray(array))
image = image.resize(tuple(list(new_shape)[::-1]), interp_code)
# Set "default" data type for reading data into NumPy array.
if image.mode == '1':
dtype_out_pil = bool
image = image.convert('L')
elif image.mode == 'L':
dtype_out_pil = np.uint8
elif image.mode == 'I':
dtype_out_pil = np.int32
elif image.mode == 'F':
dtype_out_pil = np.float32
# Convert Pillow Image to NumPy array.
array_r = np.fromstring(image.tobytes(), dtype=dtype_out_pil)
array_r = array_r.reshape((image.size[1], image.size[0]))
elif method == 'gdal':
# Set up grid coordinate arrays, then run interp2_gdal.
X = np.arange(array.shape[1]) + 1
Y = np.arange(array.shape[0]) + 1
Xi = np.linspace(X[0], X[-1] + (X[1]-X[0]), num=(new_shape[1] + 1))[0:-1]
Yi = np.linspace(Y[0], Y[-1] + (Y[1]-Y[0]), num=(new_shape[0] + 1))[0:-1]
array_r = interp2_gdal(X, Y, array, Xi, Yi, interp, extrapolate=False)
elif method == 'scipy':
PILmode = 'L' if array.dtype in (bool, np.uint8) else 'F'
if PILmode == 'L' and array.dtype != np.uint8:
array = array.astype(np.uint8)
array_r = scipy.misc.imresize(array, new_shape, interp, PILmode)
# Handle output data type and conversions.
if dtype_out == 'input' and array_r.dtype != dtype_in:
if round_proper:
array_r = astype_round_and_crop(array_r, dtype_in, allow_modify_array=True)
else:
array_r = astype_cropped(array_r, dtype_in, allow_modify_array=True)
elif dtype_out == 'float' and not np.issubdtype(array_r.dtype, np.floating):
array_r = array_r.astype(np.float32)
if one_dim_flag:
result_size_1d = new_shape[0] if one_dim_axis == 0 else new_shape[1]
array_r = np.reshape(array_r, result_size_1d)
return array_r
def imresize_pil(array, size, interp='bicubic', dtype_out='input',
float_resize=True, round_proper=True,
one_dim_axis=1):
"""
Resize an array.
Parameters
----------
array : ndarray, 2D
The array to resize.
size : shape tuple (2D) or scalar value
If shape tuple, returns an array of this size.
If scalar value, returns an array of shape
that is `size` times the shape of `array`.
interp : str; 'nearest', 'box', 'bilinear', 'hamming',
'bicubic', or 'lanczos'
Interpolation method to use during resizing.
dtype_out : str; 'default' or 'input'
If 'default' and `float_resize=True`, the returned