coord_calc.py 9.54 KB
Newer Older
Daniel Scheffler's avatar
Daniel Scheffler committed
1
2
# -*- coding: utf-8 -*-

3
4
# py_tools_ds - A collection of geospatial data analysis tools that simplify standard
# operations when handling geospatial raster and vector data as well as projections.
5
#
6
7
8
9
# Copyright (C) 2016-2021
# - Daniel Scheffler (GFZ Potsdam, daniel.scheffler@gfz-potsdam.de)
# - Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences Potsdam,
#   Germany (https://www.gfz-potsdam.de/)
10
11
12
13
14
#
# This software was developed within the context of the GeoMultiSens project funded
# by the German Federal Ministry of Education and Research
# (project grant code: 01 IS 14 010 A-C).
#
15
16
17
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
18
#
19
#   http://www.apache.org/licenses/LICENSE-2.0
20
#
21
22
23
24
25
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
26

27
import warnings
28
from typing import Iterable, Tuple  # noqa: F401
Daniel Scheffler's avatar
Daniel Scheffler committed
29
30
31
32
33
import numpy as np

# custom
from shapely.geometry import Polygon

34
__author__ = "Daniel Scheffler"
Daniel Scheffler's avatar
Daniel Scheffler committed
35
36


37
def get_corner_coordinates(gt, cols, rows):
Daniel Scheffler's avatar
Daniel Scheffler committed
38
    """Return (ULxy, LLxy, LRxy, URxy) in the same coordinate units like the given geotransform."""
39
    ext = []
40
41
    xarr = [0, cols]
    yarr = [0, rows]
Daniel Scheffler's avatar
Daniel Scheffler committed
42
43
    for px in xarr:
        for py in yarr:
44
45
            x = gt[0] + (px * gt[1]) + (py * gt[2])
            y = gt[3] + (px * gt[4]) + (py * gt[5])
46
            ext.append([x, y])
Daniel Scheffler's avatar
Daniel Scheffler committed
47
48
49
50
51
        yarr.reverse()
    return ext


def calc_FullDataset_corner_positions(mask_1bit, assert_four_corners=True, algorithm='shapely'):
52
    # type: (np.ndarray, bool, str) -> list
Daniel Scheffler's avatar
Daniel Scheffler committed
53
    """Calculate the image coordinates of the true data corners from a nodata mask.
54

55
56
    NOTE: Algorithm 'shapely' calculates the corner coordinates of the convex hull of the given mask. Since the convex
    hull not always reflects all of the true corner coordinates the result can have a limitation in this regard.
Daniel Scheffler's avatar
Daniel Scheffler committed
57
58

    :param mask_1bit:               2D-numpy array 1bit
59
60
    :param assert_four_corners:     <bool> whether to assert four corners or not
    :param algorithm:               <str> 'shapely' or 'numpy' (default: 'shapely')
Daniel Scheffler's avatar
Daniel Scheffler committed
61
62
    :return:                        [UL, UR, LL, LR] as [(ULrow,ULcol),(URrow,URcol),...]
    """
63
    # check if the mask extent covers real data or only nodata
Daniel Scheffler's avatar
Daniel Scheffler committed
64
    pixVals = np.unique(mask_1bit)
65
66
67
68
69
    if not (True in pixVals or 1 in pixVals):
        # 'According to the given mask the mask extent is completely outside the image.
        # No calculation of image corner coordinates possible.'
        return []

Daniel Scheffler's avatar
Daniel Scheffler committed
70
71
    rows, cols = mask_1bit.shape[:2]

72
    if sum([mask_1bit[0, 0], mask_1bit[0, cols - 1], mask_1bit[rows - 1, 0], mask_1bit[rows - 1, cols - 1]]) == 4:
Daniel Scheffler's avatar
Daniel Scheffler committed
73
        # if mask contains pixel value 1 in all of the four corners: return outer corner coords
74
        out = [(0, 0), (0, cols - 1), (rows - 1, 0), (rows - 1, cols - 1)]  # UL, UR, LL, LR
Daniel Scheffler's avatar
Daniel Scheffler committed
75
    else:
76
        assert algorithm in ['shapely', 'numpy'], "Algorithm '%' does not exist. Choose between 'shapely' and 'numpy'."
Daniel Scheffler's avatar
Daniel Scheffler committed
77
78
79

        if algorithm == 'shapely':
            # get outline
80
            topbottom = np.empty((1, 2 * mask_1bit.shape[1]), dtype=np.uint16)
Daniel Scheffler's avatar
Daniel Scheffler committed
81
            topbottom[0, 0:mask_1bit.shape[1]] = np.argmax(mask_1bit, axis=0)
82
83
            topbottom[0, mask_1bit.shape[1]:] = (mask_1bit.shape[0] - 1) - np.argmax(np.flipud(mask_1bit), axis=0)
            mask = np.tile(np.any(mask_1bit, axis=0), (2,))
Daniel Scheffler's avatar
Daniel Scheffler committed
84
85
86
            xvalues = np.tile(np.arange(mask_1bit.shape[1]), (1, 2))
            outline = np.vstack([topbottom, xvalues])[:, mask].T

87
            with warnings.catch_warnings():
88
                warnings.simplefilter('ignore')  # FIXME not working
89
90
                poly = Polygon(outline).convex_hull
                poly = poly.simplify(20, preserve_topology=True)
91
92
93
94
                poly = Polygon(list(set(
                    poly.exterior.coords))).convex_hull  # eliminiate duplicates except of 1 (because poly is closed)
                unique_corn_YX = sorted(set(poly.exterior.coords),
                                        key=lambda x: poly.exterior.coords[:].index(x))  # [(rows,cols),rows,cols]
Daniel Scheffler's avatar
Daniel Scheffler committed
95

96
            if assert_four_corners or len(unique_corn_YX) == 4:
Daniel Scheffler's avatar
Daniel Scheffler committed
97
                # sort calculated corners like this: [UL, UR, LL, LR]
98
99
                assert len(unique_corn_YX) == 4, \
                    'Found %s unique corner coordinates instead of 4. Exiting.' % len(unique_corn_YX)
Daniel Scheffler's avatar
Daniel Scheffler committed
100

101
102
103
104
105
106
107
108
                def get_dist(P1yx, P2yx):
                    return np.sqrt((P1yx[0] - P2yx[0]) ** 2 + (P1yx[1] - P2yx[1]) ** 2)

                def getClosest(tgtYX, srcYXs):
                    return srcYXs[np.array([get_dist(tgtYX, srcYX) for srcYX in srcYXs]).argmin()]

                out = [getClosest(tgtYX, unique_corn_YX) for tgtYX in
                       [(0, 0), (0, cols - 1), (rows - 1, 0), (rows - 1, cols - 1)]]
Daniel Scheffler's avatar
Daniel Scheffler committed
109
110
111
            else:
                out = unique_corn_YX

112
        else:  # alg='numpy'
Daniel Scheffler's avatar
Daniel Scheffler committed
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
            cols_containing_data = np.any(mask_1bit, axis=0)
            rows_containing_data = np.any(mask_1bit, axis=1)

            first_dataCol = list(cols_containing_data).index(True)
            last_dataCol = cols - (list(reversed(cols_containing_data)).index(True) + 1)
            first_dataRow = list(rows_containing_data).index(True)
            last_dataRow = rows - (list(reversed(rows_containing_data)).index(True) + 1)

            StartStopRows_in_first_dataCol = [list(mask_1bit[:, first_dataCol]).index(True),
                                              (rows - list(reversed(mask_1bit[:, first_dataCol])).index(True) + 1)]
            StartStopRows_in_last_dataCol = [list(mask_1bit[:, last_dataCol]).index(True),
                                             (rows - list(reversed(mask_1bit[:, last_dataCol])).index(True) + 1)]
            StartStopCols_in_first_dataRow = [list(mask_1bit[first_dataRow, :]).index(True),
                                              (cols - list(reversed(mask_1bit[first_dataRow, :])).index(True) + 1)]
            StartStopCols_in_last_dataRow = [list(mask_1bit[last_dataRow, :]).index(True),
                                             (cols - list(reversed(mask_1bit[last_dataRow, :])).index(True) + 1)]

            if True in [abs(np.diff(i)[0]) > 10 for i in
131
132
                        [StartStopRows_in_first_dataCol, StartStopRows_in_last_dataCol,
                         StartStopCols_in_first_dataRow, StartStopCols_in_last_dataRow]]:
133
134
                # In case of cut image corners (e.g. ALOS AVNIR-2 Level-1B2):
                # Calculation of trueDataCornerPos outside of the image.'''
Daniel Scheffler's avatar
Daniel Scheffler committed
135
136
137
138
139
140
141
142
                line_N = ((StartStopCols_in_first_dataRow[1], first_dataRow),
                          (last_dataCol, StartStopRows_in_last_dataCol[0]))
                line_S = ((first_dataCol, StartStopRows_in_first_dataCol[1]),
                          (StartStopCols_in_last_dataRow[0], last_dataRow))
                line_W = ((StartStopCols_in_first_dataRow[0], first_dataRow),
                          (first_dataCol, StartStopRows_in_first_dataCol[0]))
                line_O = ((last_dataCol, StartStopRows_in_last_dataCol[1]),
                          (StartStopCols_in_last_dataRow[1], last_dataRow))
143
                from .vector.topology import find_line_intersection_point  # import here avoids circular dependencies
144
145
146
147
                corners = [list(reversed(find_line_intersection_point(line_N, line_W))),
                           list(reversed(find_line_intersection_point(line_N, line_O))),
                           list(reversed(find_line_intersection_point(line_S, line_W))),
                           list(reversed(find_line_intersection_point(line_S, line_O)))]
Daniel Scheffler's avatar
Daniel Scheffler committed
148
149
            else:
                dataRow_in_first_dataCol = np.mean(StartStopRows_in_first_dataCol)
150
                dataRow_in_last_dataCol = np.mean(StartStopRows_in_last_dataCol)
Daniel Scheffler's avatar
Daniel Scheffler committed
151
                dataCol_in_first_dataRow = np.mean(StartStopCols_in_first_dataRow)
152
                dataCol_in_last_dataRow = np.mean(StartStopCols_in_last_dataRow)
Daniel Scheffler's avatar
Daniel Scheffler committed
153
154
155
                corners = [(first_dataRow, dataCol_in_first_dataRow), (dataRow_in_last_dataCol, last_dataCol),
                           (last_dataRow, dataCol_in_last_dataRow), (dataRow_in_first_dataCol, first_dataCol)]

156
157
158
159
160
            def getClosest(refR, refC):
                return corners[np.array([np.sqrt((refR - c[0]) ** 2 + (refC - c[1]) ** 2) for c in
                                         corners]).argmin()]  # FIXME this can also result in only 2 corners
            out = [getClosest(refR, refC) for refR, refC in
                   [(0, 0), (0, cols - 1), (rows - 1, 0), (rows - 1, cols - 1)]]
Daniel Scheffler's avatar
Daniel Scheffler committed
161
            # if UL[0] == 0 and UR[0] == 0:
162
163
            #     envi.save_image(os.path.abspath('./testing/out/__faulty_mask_1bit.hdr'),
            #                     self.mask_1bit, dtype= 'uint8', interleave='bsq', ext='.bsq',force=True)
Daniel Scheffler's avatar
Daniel Scheffler committed
164
            # print(UL,UR,LL,LR)
165
166
167
168
    return out


def corner_coord_to_minmax(corner_coords):
169
    # type: (Iterable[Tuple[float, float], Tuple[float, float], Tuple[float, float], Tuple[float, float]]) -> tuple
Daniel Scheffler's avatar
Daniel Scheffler committed
170
    """Return the bounding coordinates for a given set of XY coordinates.
171
172
173
174
175
176
177

    :param corner_coords:   # four XY tuples of corner coordinates. Their order does not matter.
    :return: xmin,xmax,ymin,ymax
    """
    x_vals = [i[0] for i in corner_coords]
    y_vals = [i[1] for i in corner_coords]
    xmin, xmax, ymin, ymax = min(x_vals), max(x_vals), min(y_vals), max(y_vals)
178

179
    return xmin, xmax, ymin, ymax