aceti 0.0.2__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.
aceti/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # from .maps_utils import png_to_csv, csv_to_png, csv_to_npy, invert_map, get_output_name, csv_shrink, csv_downsize
2
+ from .map_importer import import_map, plot_map, map_downsize, csv_shrink
3
+ from .map_creator import GoogleMapDownloader
aceti/map_creator.py ADDED
@@ -0,0 +1,273 @@
1
+ from aceti import import_map
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from PIL import Image
5
+ import math
6
+ from tqdm.auto import tqdm
7
+ import os
8
+ import urllib.request
9
+ import pyproj
10
+
11
+ class GoogleMapDownloader:
12
+ """
13
+ A class which generates high resolution google maps image given
14
+ Northwest and Southeast points in [Lat Lon] and zoom level
15
+ You can change the quality of the image by changing the zoom and layer values
16
+
17
+ 19 is a valid zoom value knowing the size of our drone
18
+
19
+ for the maps you have available
20
+ ROADMAP = "v"
21
+ TERRAIN = "p"
22
+ ALTERED_ROADMAP = "r"
23
+ SATELLITE = "s"
24
+ TERRAIN_ONLY = "t"
25
+ HYBRID = "y"
26
+
27
+
28
+ """
29
+
30
+ def __init__(self, Northwest, Southeast, zoom=19, layer="s", server="google"):
31
+ """
32
+ GoogleMapDownloader Constructor
33
+ Args:
34
+ lat: The latitude of the location required
35
+ lng: The longitude of the location required
36
+ zoom: The zoom level of the location required, ranges from 0 - 23
37
+ """
38
+ self.Northwest = Northwest
39
+ self.Southeast = Southeast
40
+ if zoom < 0 or zoom > 22:
41
+ raise ValueError("Zoom level must be between 0 and 22")
42
+ self._zoom = zoom
43
+ self._layer = layer
44
+
45
+ if server not in ["google", "arcgis"]:
46
+ raise ValueError("Server must be either 'google' or 'arcgis'")
47
+ self._server = server
48
+
49
+ # Set up transformers, EPSG:3857 is metric, same as EPSG:900913
50
+ self.to_proxy_transformer = pyproj.Transformer.from_crs('epsg:4326', 'epsg:3857')
51
+ self.to_original_transformer = pyproj.Transformer.from_crs('epsg:3857', 'epsg:4326')
52
+
53
+ # Check if the coordinates are valid
54
+ if not (isinstance(Northwest, np.ndarray) or isinstance(Northwest, list)) or not (isinstance(Southeast, np.ndarray) or isinstance(Southeast, list)):
55
+ raise ValueError("Northwest and Southeast must be tuples")
56
+ if Northwest[0]<=Southeast[0] or Northwest[1]>=Southeast[1]:
57
+ raise ValueError("you didn't provide a valid rectangle, check your coordinates")
58
+
59
+
60
+ self.get_tile_coordinates()
61
+
62
+ if (abs(self.nw_tile[0]-self.se_tile[0]) == 0) or (abs(self.nw_tile[1]-self.se_tile[1]) == 0):
63
+ raise ValueError("Insuficient zoom, points are too close, i am lazy to program this, increase zoom")
64
+ self.generateImage()
65
+
66
+
67
+ def get_tile_coordinates(self):
68
+ """
69
+ Generates an X,Y tile and pixel coordinate based on the latitude, longitude
70
+ and zoom level
71
+ """
72
+
73
+ tile_size = 256 #each tile has 256 pixels
74
+
75
+ # Use a left shift to get the power of 2, zoom 0 is a world map, zoom 1 is wold map divided in 4 tiles, zoom 2 is world map divided in 16 tiles
76
+ # i.e. a zoom level of 2 will have 2^2 = 4 tiles as coordinate sistem divides axis in 4 (x+y+,x-y+,x-y-,x+y-)
77
+ numTiles = 1 << self._zoom
78
+
79
+ ########################
80
+ # For Northwest corner
81
+ ########################
82
+ # Find the x_pixel given the longitude
83
+ x_pixel = (tile_size / 2 + self.Northwest[1] * tile_size / 360.0) * numTiles
84
+ # Convert the latitude to radians and take the sine
85
+ sin_y = math.sin(self.Northwest[0] * (math.pi / 180.0))
86
+ # Calulate the y_pixel
87
+ y_pixel = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) * -(tile_size / (2 * math.pi))) * numTiles
88
+
89
+ self.nw_pixel=[x_pixel, y_pixel]
90
+ self.nw_tile=[int(x_pixel // tile_size), int(y_pixel // tile_size)]
91
+
92
+ ########################
93
+ # For Southeast corner
94
+ ########################
95
+ # Find the x_pixel given the longitude
96
+ x_pixel = (tile_size / 2 + self.Southeast[1] * tile_size / 360.0) * numTiles
97
+ # Convert the latitude to radians and take the sine
98
+ sin_y = math.sin(self.Southeast[0] * (math.pi / 180.0))
99
+ # Calulate the y_pixel
100
+ y_pixel = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) * -(tile_size / (2 * math.pi))) * numTiles
101
+
102
+ self.se_pixel=[x_pixel, y_pixel]
103
+ self.se_tile=[math.ceil(x_pixel // tile_size)+1, math.ceil(y_pixel // tile_size)+1]
104
+
105
+ def generateImage(self):
106
+ """
107
+ Generates an image by stitching a number of google map tiles together. after executing self.get_tile_coordinates
108
+ Returns:
109
+ A high-resolution Goole Map image.
110
+ """
111
+
112
+ tile_width = abs(self.nw_tile[0]-self.se_tile[0])
113
+ tile_height = abs(self.nw_tile[1]-self.se_tile[1])
114
+
115
+ # Determine the size of the image, for simplicity, we will print the whole image and later crop it
116
+ width, height = 256 * tile_width, 256 * tile_height
117
+
118
+ # Create a new image of the size required
119
+ map_img = Image.new('RGB', (width, height))
120
+
121
+
122
+ # calculate northwest pixel of the tile
123
+ start_x=self.nw_tile[0]
124
+ start_y=self.nw_tile[1]
125
+
126
+ # print the map and report using tdqm
127
+ pbar = tqdm(total=tile_width*tile_height, unit="tiles")
128
+ pbar.set_description("Downloading map")
129
+ pbar.set_postfix_str(f"Zoom: {self._zoom}, Layer: {self._layer}")
130
+ pbar.refresh()
131
+ for x in range(0, tile_width):
132
+ for y in range(0, tile_height):
133
+ current_tile = str(x) + '-' + str(y)
134
+ try:
135
+ if self._server == "google":
136
+ url = f'https://mt0.google.com/vt?lyrs={self._layer}&x=' + str(start_x + x) + '&y=' + str(start_y + y) + '&z=' + str(self._zoom)
137
+ urllib.request.urlretrieve(url, current_tile)
138
+ elif self._server == "arcgis":
139
+ url = f"http://services.arcgisonline.com/ArcGis/rest/services/World_Imagery/MapServer/tile/{self._zoom}/{start_y + y}/{start_x + x}.png"
140
+ urllib.request.urlretrieve(url, current_tile)
141
+ else:
142
+ raise ValueError("Server not available")
143
+ except :
144
+ raise ValueError("Server not available")
145
+ im = Image.open(current_tile)
146
+ map_img.paste(im, (x * 256, y * 256))
147
+
148
+ os.remove(current_tile)
149
+ pbar.update(1)
150
+ pbar.refresh()
151
+
152
+ print("map downloaded")
153
+
154
+ self.raw_map_img = map_img.copy()
155
+ # cut corners
156
+ left=abs(self.nw_tile[0] * 256 - self.nw_pixel[0])
157
+ top=abs(self.nw_tile[1] * 256 - self.nw_pixel[1])
158
+ right=left + abs(self.se_pixel[0] - self.nw_pixel[0])
159
+ bottom=top + abs(self.nw_pixel[1] - self.se_pixel[1])
160
+
161
+
162
+ map_img = map_img.crop((left, top, right, bottom))
163
+ self.map_img = map_img
164
+
165
+
166
+ def to_pixel(self, GPS_point):
167
+ """
168
+ Generates an X,Y pixel coordinate based on the latitude, longitude
169
+ and zoom level
170
+ Returns: An X,Y pixel coordinate
171
+ """
172
+ #check if we are inside the map
173
+ self.pole=[None,None]
174
+ if self.Northwest[0]>self.Southeast[0]:
175
+ self.pole[0]="N"
176
+ else:
177
+ self.pole[0]="S"
178
+ if self.Northwest[1]>self.Southeast[1]:
179
+ self.pole[1]="W"
180
+ else:
181
+ self.pole[1]="E"
182
+
183
+
184
+ if self.pole[1]=="W":
185
+ if ((self.Southeast[1])>(GPS_point[1])>(self.Northwest[1])):
186
+ raise ValueError(f"point {GPS_point} 1W is outside the map {[self.Northwest,self.Southeast]}")
187
+ else:
188
+ if ((self.Southeast[1])<(GPS_point[1])<(self.Northwest[1])):
189
+ raise ValueError(f"point {GPS_point} 1E is outside the map {[self.Northwest,self.Southeast]}")
190
+ if self.pole[0]=="N":
191
+ if ((self.Southeast[0])>(GPS_point[0])>(self.Northwest[0])):
192
+ raise ValueError(f"point {GPS_point} 0N is outside the map {[self.Northwest,self.Southeast]}")
193
+ else:
194
+ if ((self.Southeast[0])<(GPS_point[0])<(self.Northwest[0])):
195
+ raise ValueError(f"point {GPS_point} 0S is outside the map {[self.Northwest,self.Southeast]}")
196
+
197
+
198
+
199
+ tile_size = 256 #each tile has 256 pixels
200
+
201
+ # Use a left shift to get the power of 2, zoom 0 is a world map, zoom 1 is wold map divided in 4 tiles, zoom 2 is world map divided in 16 tiles
202
+ # i.e. a zoom level of 2 will have 2^2 = 4 tiles as coordinate sistem divides axis in 4 (x+y+,x-y+,x-y-,x+y-)
203
+ numTiles = 1 << self._zoom
204
+
205
+ # Find the x_pixel given the longitude
206
+ x_pixel = (tile_size / 2 + GPS_point[1] * tile_size / 360.0) * numTiles
207
+
208
+ # Convert the latitude to radians and take the sine
209
+ sin_y = math.sin(GPS_point[0] * (math.pi / 180.0))
210
+
211
+ # Calulate the y_pixel
212
+ y_pixel = ((tile_size / 2) + 0.5 * math.log((1 + sin_y) / (1 - sin_y)) * -(
213
+ tile_size / (2 * math.pi))) * numTiles
214
+
215
+ return [x_pixel-self.nw_pixel[0], y_pixel-self.nw_pixel[1]]
216
+
217
+ def show_grid(self, lat_lon_map, base_matrix):
218
+
219
+ # if lat_lon_map.shape[0] == base_matrix.shape[0] and lat_lon_map.shape[1] == base_matrix.shape[1]:
220
+ # print("base_matrix and lat_lon_map have the same shape, extending last column and row")
221
+ # # Extend the last column and row of lat_lon_map
222
+ # lat_lon_map = np.concatenate((lat_lon_map, lat_lon_map[-1:, :]), axis=0)
223
+ # lat_lon_map = np.concatenate((lat_lon_map, lat_lon_map[:, -1:]), axis=1)
224
+ # for i in range(lat_lon_map.shape[0]):
225
+ # lat_lon_map[i, -1] = lat_lon_map[i, -2] +( lat_lon_map[i, -2] - lat_lon_map[i, -3])
226
+ # for i in range(lat_lon_map.shape[1]):
227
+ # lat_lon_map[-1, i] = lat_lon_map[-2, i] + (lat_lon_map[-2, i] - lat_lon_map[-3, i])
228
+ # print("lat_lon_map shape: ", lat_lon_map.shape)
229
+
230
+
231
+ # cut corners
232
+ p1=self.to_pixel(lat_lon_map[0, 0])
233
+ p2=self.to_pixel(lat_lon_map[1, 1])
234
+ distance=min(abs(p1[0]-p2[0]),abs(p1[1]-p2[1]))
235
+ left=abs(self.nw_tile[0] * 256 - self.nw_pixel[0] -distance)
236
+ top=abs(self.nw_tile[1] * 256 - self.nw_pixel[1] +distance)
237
+ right=left + abs(self.se_pixel[0] - self.nw_pixel[0] + distance)
238
+ bottom=top + abs(self.nw_pixel[1] - self.se_pixel[1] -distance)
239
+ print("distance: ", distance)
240
+
241
+
242
+ map_img = self.raw_map_img.crop((left, top, right, bottom))
243
+
244
+
245
+ plt.figure()
246
+ plt.imshow(self.map_img, cmap=plt.get_cmap('binary'))
247
+ #obtain xy points of the base matrix
248
+ points = np.argwhere(base_matrix == 1)
249
+
250
+ for point in points:
251
+ try:
252
+ x=[]
253
+ y=[]
254
+ center = self.to_pixel(lat_lon_map[point[0], point[1]])
255
+ p1=[center[0]-distance/2, center[1]-distance/2]
256
+ p2=[center[0]+distance/2, center[1]-distance/2]
257
+ p3=[center[0]-distance/2, center[1]+distance/2]
258
+ p4=[center[0]+distance/2, center[1]+distance/2]
259
+ x.append(p1[0])
260
+ y.append(p1[1])
261
+ x.append(p2[0])
262
+ y.append(p2[1])
263
+ x.append(p4[0])
264
+ y.append(p4[1])
265
+ x.append(p3[0])
266
+ y.append(p3[1])
267
+ x.append(p1[0])
268
+ y.append(p1[1])
269
+ plt.plot(x,y,'b-', linewidth=distance/20)
270
+ except Exception as e:
271
+ continue
272
+ plt.savefig("map.png", dpi=400)
273
+ plt.show()
aceti/map_importer.py ADDED
@@ -0,0 +1,107 @@
1
+ import numpy as np
2
+ import os
3
+ import sys
4
+ from matplotlib import pyplot as plt
5
+
6
+
7
+ if sys.version_info < (3, 9):
8
+ # importlib.resources either doesn't exist or lacks the files() function, so use the PyPI version:
9
+ import importlib_resources
10
+ else:
11
+ # importlib.resources has files(), so use that:
12
+ import importlib.resources as importlib_resources
13
+ pkg = importlib_resources.files("aceti")
14
+
15
+ def import_map(map_name: str):
16
+ """
17
+ Import a map from a file and return the base matrix and lat/lon map.
18
+ Args:
19
+ map_name (str): The name of the map to import.
20
+ Returns:
21
+ tuple: A tuple containing the base matrix and lat/lon map.
22
+ """
23
+ map_name = map_name.lower()
24
+ if os.path.exists(pkg.joinpath("maps", f"{map_name}mask.npy")):
25
+ print(f"Loading map {map_name} from package directory")
26
+ else:
27
+ raise FileNotFoundError(f"Map {map_name} not found in package directory")
28
+
29
+ base_matrix = np.load(pkg.joinpath("maps", f"{map_name}mask.npy"))
30
+
31
+ # Load the lat/lon map
32
+ lat_lon_map = np.load(pkg.joinpath("maps", f"{map_name}latlon.npy"))
33
+
34
+ return base_matrix, lat_lon_map
35
+
36
+
37
+ def plot_map(map_name):
38
+ ''' Convert a CSV file to a binary image. '''
39
+ if isinstance(map_name, str):
40
+ binary_image = np.load(pkg.joinpath("maps", f"{map_name}mask.npy"))
41
+ elif isinstance(map_name, np.ndarray):
42
+ #check map is 1 and 0
43
+ if np.all(np.isin(map_name, [0, 1])):
44
+ binary_image = map_name
45
+ else:
46
+ raise ValueError("map_name must be a string or a numpy array of 0s and 1s")
47
+ else:
48
+ raise ValueError("map_name must be a string or a numpy array")
49
+ # Load the CSV file
50
+ binary_image = np.load(pkg.joinpath("maps", f"{map_name}mask.npy"))
51
+
52
+ # Convert the binary image to 0s and 255s
53
+ binary_image = np.where(binary_image > 0, 255, 0)
54
+
55
+ plt.imshow(binary_image, cmap='gray')
56
+ plt.axis('off')
57
+ plt.show()
58
+
59
+ def map_downsize(map_name, factor=2):
60
+ ''' Downsize a map. '''
61
+ if isinstance(map_name, str):
62
+ binary_image = np.load(pkg.joinpath("maps", f"{map_name}mask.npy"))
63
+ elif isinstance(map_name, np.ndarray):
64
+ binary_image = map_name
65
+ else:
66
+ raise ValueError("map_name must be a string or a numpy array")
67
+ # Downsize the binary image
68
+ return binary_image[::factor, ::factor]
69
+
70
+ def csv_shrink(map_name, init_column=0, final_column=None, init_row=0, final_row=None):
71
+ ''' Shrink a map. '''
72
+ if isinstance(map_name, str):
73
+ binary_image = np.load(pkg.joinpath("maps", f"{map_name}mask.npy"))
74
+ elif isinstance(map_name, np.ndarray):
75
+ binary_image = map_name
76
+ else:
77
+ raise ValueError("map_name must be a string or a numpy array")
78
+
79
+ # Set the final column and row if they are None
80
+ if final_column is None:
81
+ final_column = binary_image.shape[1]
82
+ if final_row is None:
83
+ final_row = binary_image.shape[0]
84
+
85
+ # Shrink the binary image
86
+ return binary_image[init_row:final_row, init_column:final_column]
87
+
88
+ def plot_grid(map):
89
+ ''' Plot a grid on top of a map. '''
90
+
91
+ #check map properties
92
+ if isinstance(map, str):
93
+ base_matrix, lat_lon_map = import_map(map)
94
+ elif isinstance(map, tuple):
95
+ base_matrix, lat_lon_map = map
96
+ if isinstance(base_matrix, np.ndarray) and isinstance(lat_lon_map, np.ndarray):
97
+ if not np.all(np.isin(base_matrix, [0, 1])):
98
+ raise ValueError("base_matrix must be a numpy array of 0s and 1s")
99
+ if base_matrix.shape != lat_lon_map.shape:
100
+ raise ValueError("base_matrix and lat_lon_map must have the same shape")
101
+ if not np.all(np.apply_along_axis(lambda x: isinstance(x, tuple) and len(x) == 2 and all(isinstance(i, float) for i in x), 1, lat_lon_map)):
102
+ raise ValueError("lat_lon_map must be a numpy array of tuples of 2 coordinates, [lat, lon]")
103
+ else:
104
+ raise ValueError("map must be a string or a tuple of [base_matrix, lat_lon_map]")
105
+ else:
106
+ raise ValueError("map must be a string or a tuple of [base_matrix, lat_lon_map]")
107
+ # Plot the map
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file