topo_map_processor 0.1.3__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.
- topo_map_processor/__init__.py +1844 -0
- topo_map_processor/_version.py +21 -0
- topo_map_processor/tools/__init__.py +0 -0
- topo_map_processor/tools/collect_bounds.py +23 -0
- topo_map_processor/tools/partition.py +361 -0
- topo_map_processor/tools/retile.py +311 -0
- topo_map_processor/tools/tile.py +86 -0
- topo_map_processor/tools/tile_sources.py +170 -0
- topo_map_processor/tools/update_bounds.py +53 -0
- topo_map_processor-0.1.3.dist-info/METADATA +187 -0
- topo_map_processor-0.1.3.dist-info/RECORD +14 -0
- topo_map_processor-0.1.3.dist-info/WHEEL +4 -0
- topo_map_processor-0.1.3.dist-info/entry_points.txt +6 -0
- topo_map_processor-0.1.3.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,1844 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import pickle
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from functools import cmp_to_key
|
|
11
|
+
from collections import namedtuple
|
|
12
|
+
|
|
13
|
+
import cv2
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
import pytesseract
|
|
17
|
+
import easyocr
|
|
18
|
+
from PIL import Image
|
|
19
|
+
from imgcat import imgcat
|
|
20
|
+
|
|
21
|
+
from shapely.ops import unary_union, nearest_points
|
|
22
|
+
from shapely.geometry import (
|
|
23
|
+
LineString, Polygon, box, LinearRing,
|
|
24
|
+
CAP_STYLE, JOIN_STYLE, Point
|
|
25
|
+
)
|
|
26
|
+
from shapely.affinity import translate
|
|
27
|
+
from rasterio.control import GroundControlPoint
|
|
28
|
+
from rasterio.transform import GCPTransformer
|
|
29
|
+
|
|
30
|
+
# remove decompression_bomb_check
|
|
31
|
+
Image.MAX_IMAGE_PIXELS = None
|
|
32
|
+
|
|
33
|
+
easy_ocr_reader = None
|
|
34
|
+
|
|
35
|
+
# rotation_reversal_params
|
|
36
|
+
RotationReversalParams = namedtuple('RotationReversalParams', ['rotated', 'angle', 'rotated_shape', 'original_shape'])
|
|
37
|
+
|
|
38
|
+
LineRemovalParams = namedtuple('LineRemovalParams', ['line_buf_ratio', 'blur_buf_ratio', 'blur_kern_ratio', 'blur_repeat'])
|
|
39
|
+
|
|
40
|
+
class GCPBasedTransformer:
|
|
41
|
+
def __init__(self, gcps):
|
|
42
|
+
self.pixel_map = {}
|
|
43
|
+
self.cooord_map = {}
|
|
44
|
+
gcp_objs = []
|
|
45
|
+
|
|
46
|
+
for gcp in gcps:
|
|
47
|
+
corner = gcp[0]
|
|
48
|
+
idx = gcp[1]
|
|
49
|
+
#print(f'adding gcp: {corner} -> {idx}')
|
|
50
|
+
self.pixel_map[(corner[1], corner[0])] = (idx[0], idx[1])
|
|
51
|
+
self.cooord_map[(idx[0], idx[1])] = (corner[0], corner[1])
|
|
52
|
+
gcp_obj = GroundControlPoint(row=corner[1], col=corner[0], x=idx[0], y=idx[1])
|
|
53
|
+
gcp_objs.append(gcp_obj)
|
|
54
|
+
|
|
55
|
+
self.transformer = GCPTransformer(gcp_objs, tps=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def xy(self, row, col, offset='center'):
|
|
59
|
+
if (row, col) in self.pixel_map:
|
|
60
|
+
return self.pixel_map[(row, col)]
|
|
61
|
+
|
|
62
|
+
x, y = self.transformer.xy([row], [col], offset=offset)
|
|
63
|
+
return (x[0].item(), y[0].item())
|
|
64
|
+
|
|
65
|
+
def rowcol(self, x, y):
|
|
66
|
+
if (x, y) in self.cooord_map:
|
|
67
|
+
return self.cooord_map[(x, y)]
|
|
68
|
+
rs, cs = self.transformer.rowcol([x], [y])
|
|
69
|
+
return (cs[0].item(), rs[0].item())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TopoMapProcessor:
|
|
73
|
+
def __init__(self, filepath, extra, index_map):
|
|
74
|
+
self.filepath = filepath
|
|
75
|
+
self.index_map = index_map
|
|
76
|
+
|
|
77
|
+
self.SHOW_IMG = os.getenv('SHOW_IMG', '0') == '1'
|
|
78
|
+
self.INSPECT = os.getenv('INSPECT', '0') == '1'
|
|
79
|
+
|
|
80
|
+
self.full_img = None
|
|
81
|
+
self.small_img = None
|
|
82
|
+
self.mapbox_corners = None
|
|
83
|
+
self.rotation_reversal_params = None
|
|
84
|
+
|
|
85
|
+
self.extents = extra.get('extents', None)
|
|
86
|
+
self.pixel_cutlines = extra.get('pixel_cutlines', [])
|
|
87
|
+
|
|
88
|
+
# rotation related
|
|
89
|
+
self.auto_rotate_thresh = extra.get('auto_rotate_thresh', 0.0)
|
|
90
|
+
|
|
91
|
+
# shrinking related
|
|
92
|
+
self.resize_factor = extra.get('resize_factor', 0.25)
|
|
93
|
+
self.resize_width = extra.get('resize_width', None)
|
|
94
|
+
self.resize_height = extra.get('resize_height', None)
|
|
95
|
+
if self.resize_width is not None and self.resize_height is not None:
|
|
96
|
+
raise ValueError("Either resize_width or resize_height should be set, not both.")
|
|
97
|
+
if self.resize_width is not None or self.resize_height is not None:
|
|
98
|
+
self.resize_factor = None
|
|
99
|
+
|
|
100
|
+
# mapframe location related
|
|
101
|
+
self.band_color = extra.get('band_color', 'black')
|
|
102
|
+
self.band_color_choices = []
|
|
103
|
+
self.collar_erode = extra.get('collar_erode', -2)
|
|
104
|
+
self.use_bbox_area = extra.get('use_bbox_area', True)
|
|
105
|
+
self.shrunk_map_area_corners = extra.get('shrunk_map_area_corners', None)
|
|
106
|
+
self.poly_approx_factor = extra.get('poly_approx_factor', 0.001)
|
|
107
|
+
|
|
108
|
+
# corners related
|
|
109
|
+
self.corner_ratio = extra.get('corner_ratio', 400.0 / 9000.0)
|
|
110
|
+
self.corner_overrides = extra.get('corner_overrides', None)
|
|
111
|
+
|
|
112
|
+
# text removal related
|
|
113
|
+
self.text_removal_engine = extra.get('text_removal_engine', 'tesseract')
|
|
114
|
+
self.text_removal_border = extra.get('text_removal_border', 0)
|
|
115
|
+
self.text_removal_iterations = extra.get('text_removal_iterations', 1)
|
|
116
|
+
self.text_removal_char_size_cutoff = extra.get('text_removal_char_size_cutoff', 30)
|
|
117
|
+
self.text_removal_confidence_cutoff = extra.get('text_removal_confidence_cutoff', 20)
|
|
118
|
+
self.text_removal_word_char_overlap_size_ratio_cutoff = extra.get('text_removal_word_char_overlap_size_ratio_cutoff', 0.8)
|
|
119
|
+
|
|
120
|
+
# grid lines related
|
|
121
|
+
self.should_remove_grid_lines = extra.get('should_remove_grid_lines', False)
|
|
122
|
+
# leaving it around for what a sample config looks like
|
|
123
|
+
#self.remove_line_buf_ratio = extra.get('remove_line_buf_ratio', 2.0 / 6500.0)
|
|
124
|
+
#self.remove_line_blur_buf_ratio = extra.get('remove_line_blur_buf_ratio', 14.0 / 6500.0)
|
|
125
|
+
#self.remove_line_blur_kern_ratio = extra.get('remove_line_blur_kern_ratio', 9.0 / 6500.0)
|
|
126
|
+
#self.remove_line_blur_repeat = extra.get('remove_line_blur_repeat', 2)
|
|
127
|
+
|
|
128
|
+
# georef related
|
|
129
|
+
self.jpeg_export_quality = extra.get('jpeg_export_quality', 75)
|
|
130
|
+
self.warp_jpeg_export_quality = extra.get('warp_jpeg_export_quality', 75)
|
|
131
|
+
self.warp_output_height = extra.get('warp_output_height', 6500)
|
|
132
|
+
|
|
133
|
+
self.color_map = {
|
|
134
|
+
'black': ((0, 0, 0), (179, 255, 130)),
|
|
135
|
+
'greyish': ((0, 0, 50), (179, 150, 192)),
|
|
136
|
+
'white': ((0, 0, 200), (179, 60, 255)),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_id(self):
|
|
141
|
+
ext = self.filepath.suffix
|
|
142
|
+
return self.filepath.name.replace(ext, '')
|
|
143
|
+
|
|
144
|
+
def get_sheet_ibox(self, sheet_id=None):
|
|
145
|
+
if sheet_id is None:
|
|
146
|
+
sheet_id = self.get_id()
|
|
147
|
+
|
|
148
|
+
return self.index_map[sheet_id]
|
|
149
|
+
|
|
150
|
+
def get_data_dir(self):
|
|
151
|
+
return Path('data')
|
|
152
|
+
|
|
153
|
+
def get_inter_dir(self):
|
|
154
|
+
return self.get_data_dir() / 'inter'
|
|
155
|
+
|
|
156
|
+
def get_workdir(self):
|
|
157
|
+
return self.get_inter_dir() / self.get_id()
|
|
158
|
+
|
|
159
|
+
def get_export_dir(self):
|
|
160
|
+
return Path('export/')
|
|
161
|
+
|
|
162
|
+
def get_gtiff_dir(self):
|
|
163
|
+
return self.get_export_dir() / 'gtiffs'
|
|
164
|
+
|
|
165
|
+
def get_bounds_dir(self):
|
|
166
|
+
return self.get_export_dir() / 'bounds'
|
|
167
|
+
|
|
168
|
+
def get_export_file(self, extra=None):
|
|
169
|
+
export_dir = self.get_gtiff_dir()
|
|
170
|
+
sheet_no = self.get_id()
|
|
171
|
+
return export_dir / f'{sheet_no}.tif'
|
|
172
|
+
|
|
173
|
+
def ensure_dir(self, d):
|
|
174
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
def get_bbox_area_simple(self, contour):
|
|
177
|
+
bbox = cv2.boundingRect(contour)
|
|
178
|
+
return bbox[2] * bbox[3]
|
|
179
|
+
|
|
180
|
+
def get_bbox_area(self, ctuple):
|
|
181
|
+
return self.get_bbox_area_simple(ctuple[0])
|
|
182
|
+
|
|
183
|
+
def get_distance(self, p1, p2):
|
|
184
|
+
p1 = np.array(p1)
|
|
185
|
+
p2 = np.array(p2)
|
|
186
|
+
return np.linalg.norm(p2 - p1)
|
|
187
|
+
|
|
188
|
+
def get_angle(self, p1, p2):
|
|
189
|
+
p1 = np.array(p1)
|
|
190
|
+
p2 = np.array(p2)
|
|
191
|
+
vector = p2 - p1
|
|
192
|
+
angle_radians = np.arctan2(vector[1], vector[0])
|
|
193
|
+
return np.degrees(angle_radians)
|
|
194
|
+
|
|
195
|
+
def crop_img(self, img, bbox):
|
|
196
|
+
x, y, w, h = bbox
|
|
197
|
+
return img[y:y+h, x:x+w]
|
|
198
|
+
|
|
199
|
+
def prompt(self):
|
|
200
|
+
if not self.INSPECT:
|
|
201
|
+
return
|
|
202
|
+
print('Press Enter to continue...')
|
|
203
|
+
input()
|
|
204
|
+
|
|
205
|
+
def prompt1(self):
|
|
206
|
+
self.prompt()
|
|
207
|
+
|
|
208
|
+
def prompt2(self):
|
|
209
|
+
self.prompt()
|
|
210
|
+
|
|
211
|
+
def run_external(self, cmd):
|
|
212
|
+
print(f'running cmd - {cmd}')
|
|
213
|
+
start = time.time()
|
|
214
|
+
res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
215
|
+
end = time.time()
|
|
216
|
+
print(f'STDOUT: {res.stdout}')
|
|
217
|
+
print(f'STDERR: {res.stderr}')
|
|
218
|
+
print(f'command took {end - start} secs to run')
|
|
219
|
+
if res.returncode != 0:
|
|
220
|
+
raise Exception(f'command {cmd} failed with exit code: {res.returncode}')
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# display methods
|
|
224
|
+
def show_image(self, img):
|
|
225
|
+
if not self.SHOW_IMG:
|
|
226
|
+
return
|
|
227
|
+
# check if it is a numpy array
|
|
228
|
+
if not isinstance(img, (Image.Image, np.ndarray)):
|
|
229
|
+
raise ValueError("Input must be a PIL Image or a numpy array")
|
|
230
|
+
|
|
231
|
+
if isinstance(img, np.ndarray):
|
|
232
|
+
pil_img = Image.fromarray(img)
|
|
233
|
+
else:
|
|
234
|
+
pil_img = img
|
|
235
|
+
imgcat(pil_img)
|
|
236
|
+
|
|
237
|
+
def show_contours(self, binary_img, contours, color=(0, 255, 0)):
|
|
238
|
+
if not self.SHOW_IMG:
|
|
239
|
+
return
|
|
240
|
+
# check if image is uint8
|
|
241
|
+
if binary_img.dtype != 'uint8':
|
|
242
|
+
raise ValueError("Input image must be a boolean array")
|
|
243
|
+
|
|
244
|
+
rgb = cv2.merge([binary_img, binary_img, binary_img])
|
|
245
|
+
cv2.drawContours(rgb, contours, -1, color, 2, cv2.LINE_AA)
|
|
246
|
+
|
|
247
|
+
self.show_image(rgb)
|
|
248
|
+
|
|
249
|
+
def show_points(self, points, img_grey, color=(0, 255, 0), radius=5):
|
|
250
|
+
if not self.SHOW_IMG:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# check if image is uint8
|
|
254
|
+
if img_grey.dtype != 'uint8':
|
|
255
|
+
raise ValueError("Input image must be a boolean array")
|
|
256
|
+
|
|
257
|
+
img_rgb = np.stack([img_grey, img_grey, img_grey], axis=-1)
|
|
258
|
+
|
|
259
|
+
h, w = img_rgb.shape[:2]
|
|
260
|
+
|
|
261
|
+
for point in points:
|
|
262
|
+
center_x = point[0]
|
|
263
|
+
center_y = point[1]
|
|
264
|
+
y_coords, x_coords = np.ogrid[:h, :w]
|
|
265
|
+
distances = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
|
|
266
|
+
circle_mask = distances <= radius
|
|
267
|
+
img_rgb[circle_mask] = color
|
|
268
|
+
|
|
269
|
+
self.show_image(img_rgb)
|
|
270
|
+
|
|
271
|
+
def save_with_points(self, points_and_colors, img, out_file, radius=2):
|
|
272
|
+
if not isinstance(img, np.ndarray):
|
|
273
|
+
raise ValueError("Input image must be a numpy array")
|
|
274
|
+
|
|
275
|
+
h, w = img.shape[:2]
|
|
276
|
+
|
|
277
|
+
img = img.copy()
|
|
278
|
+
|
|
279
|
+
for point,color in points_and_colors:
|
|
280
|
+
center_x = int(point[0])
|
|
281
|
+
center_y = int(point[1])
|
|
282
|
+
|
|
283
|
+
# Define a bounding box around the point
|
|
284
|
+
x_start = max(0, center_x - radius)
|
|
285
|
+
y_start = max(0, center_y - radius)
|
|
286
|
+
x_end = min(w, center_x + radius + 1)
|
|
287
|
+
y_end = min(h, center_y + radius + 1)
|
|
288
|
+
|
|
289
|
+
if x_start >= x_end or y_start >= y_end:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Create a smaller ogrid within the bounding box
|
|
293
|
+
y_coords, x_coords = np.ogrid[y_start:y_end, x_start:x_end]
|
|
294
|
+
|
|
295
|
+
# Calculate distances and the circle_mask only for this smaller region
|
|
296
|
+
distances = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
|
|
297
|
+
circle_mask = distances <= radius
|
|
298
|
+
|
|
299
|
+
# Apply the circle_mask to the corresponding slice of the image
|
|
300
|
+
img_slice = img[y_start:y_end, x_start:x_end]
|
|
301
|
+
img_slice[circle_mask] = color
|
|
302
|
+
|
|
303
|
+
cv2.imwrite(str(out_file), img)
|
|
304
|
+
|
|
305
|
+
def save_with_lines(self, line_and_color_pairs, img, out_file, thickness=2):
|
|
306
|
+
if not isinstance(img, np.ndarray):
|
|
307
|
+
raise ValueError("Input image must be a numpy array")
|
|
308
|
+
|
|
309
|
+
img = img.copy()
|
|
310
|
+
|
|
311
|
+
for line, color in line_and_color_pairs:
|
|
312
|
+
p1 = (int(line[0][0]), int(line[0][1]))
|
|
313
|
+
p2 = (int(line[1][0]), int(line[1][1]))
|
|
314
|
+
cv2.line(img, p1, p2, color, thickness)
|
|
315
|
+
cv2.circle(img, p1, thickness * 2, color, -1)
|
|
316
|
+
cv2.circle(img, p2, thickness * 2, color, -1)
|
|
317
|
+
|
|
318
|
+
cv2.imwrite(str(out_file), img)
|
|
319
|
+
|
|
320
|
+
def save_cutline(self, multipolygon):
|
|
321
|
+
line_and_color_pairs = []
|
|
322
|
+
colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0), (255, 255, 0), (0, 255, 255), (255, 0, 255)] # B, G, R, Y, C, M
|
|
323
|
+
color_idx = 0
|
|
324
|
+
for polygon in multipolygon.geoms:
|
|
325
|
+
# Exterior ring
|
|
326
|
+
exterior_points = list(polygon.exterior.coords)
|
|
327
|
+
current_color = colors[color_idx % len(colors)]
|
|
328
|
+
color_idx += 1
|
|
329
|
+
for i in range(len(exterior_points) - 1):
|
|
330
|
+
line_and_color_pairs.append(((exterior_points[i], exterior_points[i+1]), current_color))
|
|
331
|
+
|
|
332
|
+
# Interior rings
|
|
333
|
+
for interior_ring in polygon.interiors:
|
|
334
|
+
interior_points = list(interior_ring.coords)
|
|
335
|
+
current_color = colors[color_idx % len(colors)]
|
|
336
|
+
color_idx += 1
|
|
337
|
+
for i in range(len(interior_points) - 1):
|
|
338
|
+
line_and_color_pairs.append(((interior_points[i], interior_points[i+1]), current_color))
|
|
339
|
+
self.save_with_lines(line_and_color_pairs,
|
|
340
|
+
self.get_full_img(),
|
|
341
|
+
self.get_workdir() / 'with_full_cutline.jpg',
|
|
342
|
+
thickness=2)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# translation methods
|
|
348
|
+
def scale_bbox(self, bbox, rw, rh):
|
|
349
|
+
b = bbox
|
|
350
|
+
return (int(b[0]*rw), int(b[1]*rh), int(b[2]*rw), int(b[3]*rh))
|
|
351
|
+
|
|
352
|
+
def scale_point(self, point, rw, rh):
|
|
353
|
+
return [int(point[0]*rw), int(point[1]*rh)]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def translate_point(self, point, bbox):
|
|
357
|
+
b = bbox
|
|
358
|
+
px = point[0]
|
|
359
|
+
py = point[1]
|
|
360
|
+
return (b[0] + px, b[1] + py)
|
|
361
|
+
|
|
362
|
+
# copied form imutils and modified to take fill
|
|
363
|
+
def rotate_image_bound(self, img, angle, fill_color=(255, 255, 255)):
|
|
364
|
+
"""
|
|
365
|
+
Rotate an image by a given angle and return the rotated image.
|
|
366
|
+
The image is rotated around its center.
|
|
367
|
+
"""
|
|
368
|
+
if angle == 0:
|
|
369
|
+
return img
|
|
370
|
+
|
|
371
|
+
(h, w) = img.shape[:2]
|
|
372
|
+
(cX, cY) = (w / 2, h / 2)
|
|
373
|
+
|
|
374
|
+
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
|
|
375
|
+
cos = np.abs(M[0, 0])
|
|
376
|
+
sin = np.abs(M[0, 1])
|
|
377
|
+
|
|
378
|
+
nW = int((h * sin) + (w * cos))
|
|
379
|
+
nH = int((h * cos) + (w * sin))
|
|
380
|
+
|
|
381
|
+
M[0, 2] += (nW / 2) - cX
|
|
382
|
+
M[1, 2] += (nH / 2) - cY
|
|
383
|
+
|
|
384
|
+
rotated_img = cv2.warpAffine(img, M, (nW, nH),
|
|
385
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
386
|
+
borderValue=fill_color)
|
|
387
|
+
|
|
388
|
+
return rotated_img
|
|
389
|
+
|
|
390
|
+
def get_original_pixel_coordinate_internal(self, rotated_point, rotation_reversal_params):
|
|
391
|
+
"""
|
|
392
|
+
Given a pixel coordinate in the rotated image, gives the pixel coordinate in the original image.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
angle = rotation_reversal_params.angle
|
|
396
|
+
rotated_shape = rotation_reversal_params.rotated_shape
|
|
397
|
+
original_shape = rotation_reversal_params.original_shape
|
|
398
|
+
|
|
399
|
+
(h, w) = original_shape
|
|
400
|
+
(nH, nW) = rotated_shape
|
|
401
|
+
(cX, cY) = (w / 2, h / 2)
|
|
402
|
+
|
|
403
|
+
# Recreate the forward transformation matrix M
|
|
404
|
+
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
|
|
405
|
+
M[0, 2] += (nW / 2) - cX
|
|
406
|
+
M[1, 2] += (nH / 2) - cY
|
|
407
|
+
|
|
408
|
+
# Invert the transformation matrix
|
|
409
|
+
inv_M = cv2.invertAffineTransform(M)
|
|
410
|
+
|
|
411
|
+
# Apply the inverse transformation to the rotated point
|
|
412
|
+
x_rot, y_rot = rotated_point
|
|
413
|
+
x_orig = inv_M[0, 0] * x_rot + inv_M[0, 1] * y_rot + inv_M[0, 2]
|
|
414
|
+
y_orig = inv_M[1, 0] * x_rot + inv_M[1, 1] * y_rot + inv_M[1, 2]
|
|
415
|
+
|
|
416
|
+
x_orig = int(round(x_orig))
|
|
417
|
+
y_orig = int(round(y_orig))
|
|
418
|
+
|
|
419
|
+
if not (0 <= x_orig < w and 0 <= y_orig < h):
|
|
420
|
+
raise ValueError(f"Calculated original coordinates for {rotated_point} are out of bounds")
|
|
421
|
+
|
|
422
|
+
return (x_orig, y_orig)
|
|
423
|
+
|
|
424
|
+
def get_color_ranges(self, color):
|
|
425
|
+
if color not in self.color_map:
|
|
426
|
+
raise ValueError(f"Color '{color}' not found in color map")
|
|
427
|
+
|
|
428
|
+
lower, upper = self.color_map[color]
|
|
429
|
+
lower = np.array(lower, dtype=np.uint8)
|
|
430
|
+
upper = np.array(upper, dtype=np.uint8)
|
|
431
|
+
|
|
432
|
+
return upper, lower
|
|
433
|
+
|
|
434
|
+
def get_color_mask(self, img, color):
|
|
435
|
+
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
|
436
|
+
|
|
437
|
+
if not isinstance(color, list):
|
|
438
|
+
colors = [color]
|
|
439
|
+
else:
|
|
440
|
+
colors = color
|
|
441
|
+
|
|
442
|
+
# https://colorizer.org/ for what the HSV values look like..
|
|
443
|
+
# N.B: the scale there is H:0-359 S:0-99 V:0-99
|
|
444
|
+
# in opencv it is H:0-179 S:0-255 V:0-255
|
|
445
|
+
img_masks = []
|
|
446
|
+
for color in colors:
|
|
447
|
+
invert = False
|
|
448
|
+
if color.startswith('not_'):
|
|
449
|
+
invert = True
|
|
450
|
+
color = color[4:]
|
|
451
|
+
|
|
452
|
+
upper, lower = self.get_color_ranges(color)
|
|
453
|
+
|
|
454
|
+
img_mask = cv2.inRange(img_hsv, lower, upper)
|
|
455
|
+
if invert:
|
|
456
|
+
# if 255 chenge to 0 and vice versa
|
|
457
|
+
img_mask ^= 255
|
|
458
|
+
|
|
459
|
+
img_masks.append(img_mask)
|
|
460
|
+
|
|
461
|
+
final_mask = img_masks[0]
|
|
462
|
+
for img_mask in img_masks[1:]:
|
|
463
|
+
orred = np.logical_or(final_mask, img_mask)*255
|
|
464
|
+
final_mask = orred
|
|
465
|
+
|
|
466
|
+
return final_mask.astype(np.uint8)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def remove_line(self, line, map_img, removal_params):
|
|
470
|
+
line_buf_ratio = removal_params.line_buf_ratio
|
|
471
|
+
blur_buf_ratio = removal_params.blur_buf_ratio
|
|
472
|
+
blur_kern_ratio = removal_params.blur_kern_ratio
|
|
473
|
+
blur_repeat = removal_params.blur_repeat
|
|
474
|
+
|
|
475
|
+
h, w = map_img.shape[:2]
|
|
476
|
+
|
|
477
|
+
line_buf = round(line_buf_ratio * w)
|
|
478
|
+
blur_buf = round(blur_buf_ratio * w)
|
|
479
|
+
blur_kern = round(blur_kern_ratio * w)
|
|
480
|
+
if blur_kern % 2 == 0:
|
|
481
|
+
blur_kern += 1
|
|
482
|
+
|
|
483
|
+
limits = Polygon([(w,0), (w,h), (0,h), (0,0), (w,0)])
|
|
484
|
+
|
|
485
|
+
ls = LineString(line)
|
|
486
|
+
line_poly = ls.buffer(line_buf, resolution=1, cap_style=CAP_STYLE.flat).intersection(limits)
|
|
487
|
+
blur_poly = ls.buffer(blur_buf, resolution=1, cap_style=CAP_STYLE.flat).intersection(limits)
|
|
488
|
+
bb = blur_poly.bounds
|
|
489
|
+
bb = [ round(x) for x in bb ]
|
|
490
|
+
# restrict to a small img strip to make things less costly
|
|
491
|
+
img_strip = map_img[bb[1]:bb[3], bb[0]:bb[2]]
|
|
492
|
+
sh, sw = img_strip.shape[:2]
|
|
493
|
+
#cv2.imwrite('temp.jpg', img_strip)
|
|
494
|
+
|
|
495
|
+
line_poly_t = translate(line_poly, xoff=-bb[0], yoff=-bb[1])
|
|
496
|
+
mask = np.zeros(img_strip.shape[:2], dtype=np.uint8)
|
|
497
|
+
poly_coords = np.array([ [int(x[0]), int(x[1])] for x in line_poly_t.exterior.coords ])
|
|
498
|
+
cv2.fillPoly(mask, pts=[poly_coords], color=1)
|
|
499
|
+
|
|
500
|
+
#img_blurred = cv2.medianBlur(img_strip, blur_kern)
|
|
501
|
+
pad = int(blur_kern/2)
|
|
502
|
+
img_strip_padded = cv2.copyMakeBorder(img_strip, pad, pad, pad, pad, cv2.BORDER_REFLECT_101)
|
|
503
|
+
|
|
504
|
+
img_blurred_padded = cv2.medianBlur(img_strip_padded, blur_kern)
|
|
505
|
+
for i in range(blur_repeat):
|
|
506
|
+
img_blurred_padded = cv2.medianBlur(img_blurred_padded, blur_kern)
|
|
507
|
+
img_blurred = img_blurred_padded[pad:pad+sh, pad:pad+sw]
|
|
508
|
+
#cv2.imwrite('temp.jpg', img_blurred)
|
|
509
|
+
|
|
510
|
+
img_strip[mask == 1] = img_blurred[mask == 1]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# from camelot.. this has served me well
|
|
515
|
+
# https://github.com/camelot-dev/camelot/blob/master/camelot/image_processing.py
|
|
516
|
+
def find_lines(self, threshold, direction, line_scale, iterations):
|
|
517
|
+
if direction not in ["vertical", "horizontal"]:
|
|
518
|
+
raise ValueError("Direction must be either 'vertical' or 'horizontal'")
|
|
519
|
+
|
|
520
|
+
lines = []
|
|
521
|
+
|
|
522
|
+
if direction == "vertical":
|
|
523
|
+
size = threshold.shape[0] // line_scale
|
|
524
|
+
el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size))
|
|
525
|
+
elif direction == "horizontal":
|
|
526
|
+
size = threshold.shape[1] // line_scale
|
|
527
|
+
el = cv2.getStructuringElement(cv2.MORPH_RECT, (size, 1))
|
|
528
|
+
elif direction is None:
|
|
529
|
+
raise ValueError("Specify direction as either 'vertical' or 'horizontal'")
|
|
530
|
+
|
|
531
|
+
threshold = cv2.erode(threshold, el)
|
|
532
|
+
#imgcat(Image.fromarray(threshold))
|
|
533
|
+
threshold = cv2.dilate(threshold, el)
|
|
534
|
+
dmask = cv2.dilate(threshold, el, iterations=iterations)
|
|
535
|
+
#imgcat(Image.fromarray(dmask))
|
|
536
|
+
|
|
537
|
+
contours, _ = cv2.findContours(
|
|
538
|
+
threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
for c in contours:
|
|
542
|
+
x, y, w, h = cv2.boundingRect(c)
|
|
543
|
+
x1, x2 = x, x + w
|
|
544
|
+
y1, y2 = y, y + h
|
|
545
|
+
if direction == "vertical":
|
|
546
|
+
lines.append(((x1 + x2) // 2, y2, (x1 + x2) // 2, y1))
|
|
547
|
+
elif direction == "horizontal":
|
|
548
|
+
lines.append((x1, (y1 + y2) // 2, x2, (y1 + y2) // 2))
|
|
549
|
+
|
|
550
|
+
return dmask, lines
|
|
551
|
+
|
|
552
|
+
def get_text_mask_easyocr(self, img, border, iterations):
|
|
553
|
+
global easy_ocr_reader
|
|
554
|
+
|
|
555
|
+
if easy_ocr_reader is None:
|
|
556
|
+
easy_ocr_reader = easyocr.Reader(['en'], gpu=False)
|
|
557
|
+
|
|
558
|
+
mask = np.zeros(img.shape[:2], dtype=np.uint8)
|
|
559
|
+
|
|
560
|
+
# Detect text in the image
|
|
561
|
+
print("Detecting text...")
|
|
562
|
+
results = easy_ocr_reader.readtext(img, ycenter_ths=0.0, width_ths=0.0, link_threshold=100.0)
|
|
563
|
+
|
|
564
|
+
if not results:
|
|
565
|
+
return mask
|
|
566
|
+
|
|
567
|
+
print(f"Found {len(results)} text regions:")
|
|
568
|
+
|
|
569
|
+
for (bbox, text, confidence) in results:
|
|
570
|
+
print(f"Text: '{text}' (Confidence: {confidence:.2f})")
|
|
571
|
+
|
|
572
|
+
# Convert bbox to integer coordinates
|
|
573
|
+
points = np.array(bbox, dtype=np.int32)
|
|
574
|
+
|
|
575
|
+
# Create a filled polygon on the mask
|
|
576
|
+
cv2.fillPoly(mask, [points], 255)
|
|
577
|
+
|
|
578
|
+
if border == 0:
|
|
579
|
+
continue
|
|
580
|
+
# Optional: Add some padding around text regions
|
|
581
|
+
# This helps remove text more completely
|
|
582
|
+
if border > 0:
|
|
583
|
+
kernel = np.ones((border, border), np.uint8)
|
|
584
|
+
mask = cv2.dilate(mask, kernel, iterations=iterations)
|
|
585
|
+
else:
|
|
586
|
+
kernel = np.ones((-border, -border), np.uint8)
|
|
587
|
+
mask = cv2.erode(mask, kernel, iterations=iterations)
|
|
588
|
+
|
|
589
|
+
return mask
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def get_text_mask_tesseract(self, img,
|
|
593
|
+
border, iterations,
|
|
594
|
+
char_size_cutoff, confidence_cutoff,
|
|
595
|
+
word_char_overlap_size_ratio_cutoff):
|
|
596
|
+
#display = img.copy()
|
|
597
|
+
h = img.shape[0]
|
|
598
|
+
mask = np.zeros(img.shape[:2], dtype=np.uint8)
|
|
599
|
+
# get word bboxes
|
|
600
|
+
word_shapes = []
|
|
601
|
+
d = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT)
|
|
602
|
+
n_boxes = len(d['level'])
|
|
603
|
+
for i in range(n_boxes):
|
|
604
|
+
# Extract bounding box coordinates and text
|
|
605
|
+
(x, y, ww, wh) = (d['left'][i], d['top'][i], d['width'][i], d['height'][i])
|
|
606
|
+
text = d['text'][i]
|
|
607
|
+
conf = int(float(d['conf'][i]))
|
|
608
|
+
print(f"Text: '{text}', x: {x}, y: {y}, height: {wh}, width: {ww}, confidence: {conf}")
|
|
609
|
+
if text.strip() == "":
|
|
610
|
+
continue
|
|
611
|
+
if ww == 0 or wh == 0:
|
|
612
|
+
continue
|
|
613
|
+
if conf > confidence_cutoff:
|
|
614
|
+
#cv2.rectangle(display, (x, y), (x + ww, y + wh), (0, 255, 0), 2)
|
|
615
|
+
word_shapes.append(box(x, y, x+ww, y+wh))
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# get char bboxes
|
|
619
|
+
d = pytesseract.image_to_boxes(img, output_type=pytesseract.Output.DICT)
|
|
620
|
+
if 'char' not in d:
|
|
621
|
+
return mask
|
|
622
|
+
print(f"Found {len(d['char'])} char regions:")
|
|
623
|
+
|
|
624
|
+
char_bboxes = []
|
|
625
|
+
for i in range(len(d['char'])):
|
|
626
|
+
(char,x1,y2,x2,y1) = (d['char'][i], d['left'][i], d['top'][i], d['right'][i], d['bottom'][i])
|
|
627
|
+
|
|
628
|
+
cw = x2 - x1
|
|
629
|
+
ch = y2 - y1
|
|
630
|
+
print(f"Text: '{char}', height: {ch}, width: {cw}, x1: {x1}, y1: {y1}, x2: {x2}, y2: {y2}")
|
|
631
|
+
|
|
632
|
+
if ch > char_size_cutoff or cw > char_size_cutoff:
|
|
633
|
+
continue
|
|
634
|
+
if char.strip() == "":
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
if cw == 0 or ch == 0:
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
char_shape = box(x1, h - y1, x2, h - y2)
|
|
641
|
+
carea = char_shape.area
|
|
642
|
+
found_word = None
|
|
643
|
+
for i, word_shape in enumerate(word_shapes):
|
|
644
|
+
warea = word_shape.area
|
|
645
|
+
if carea / warea > 1.1:
|
|
646
|
+
continue
|
|
647
|
+
if char_shape.intersects(word_shape):
|
|
648
|
+
overlap = char_shape.intersection(word_shape)
|
|
649
|
+
if overlap.area / carea > word_char_overlap_size_ratio_cutoff:
|
|
650
|
+
found_word = word_shape
|
|
651
|
+
break
|
|
652
|
+
if found_word is None:
|
|
653
|
+
continue
|
|
654
|
+
|
|
655
|
+
#cv2.rectangle(display, (x1, h - y1), (x2, h - y2), (255, 0, 0), 2)
|
|
656
|
+
char_bboxes.append((x1, y1, cw, ch))
|
|
657
|
+
# Create a rectangle on the mask
|
|
658
|
+
cv2.rectangle(mask, (x1, h - y1), (x2, h - y2), 255, thickness=cv2.FILLED)
|
|
659
|
+
|
|
660
|
+
if len(char_bboxes) > 0:
|
|
661
|
+
self.show_image(mask)
|
|
662
|
+
#self.show_image(display)
|
|
663
|
+
|
|
664
|
+
if border == 0:
|
|
665
|
+
return mask
|
|
666
|
+
|
|
667
|
+
# Optional: Add some padding around text regions
|
|
668
|
+
# This helps remove text more completely
|
|
669
|
+
if border > 0:
|
|
670
|
+
kernel = np.ones((border, border), np.uint8)
|
|
671
|
+
mask = cv2.dilate(mask, kernel, iterations=iterations)
|
|
672
|
+
else:
|
|
673
|
+
kernel = np.ones((-border, -border), np.uint8)
|
|
674
|
+
mask = cv2.erode(mask, kernel, iterations=iterations)
|
|
675
|
+
|
|
676
|
+
return mask
|
|
677
|
+
|
|
678
|
+
def get_transformer_from_gcps(self, gcps):
|
|
679
|
+
return GCPBasedTransformer(gcps)
|
|
680
|
+
|
|
681
|
+
def get_full_file_path(self):
|
|
682
|
+
workdir = self.get_workdir()
|
|
683
|
+
rotated_img_file = workdir / 'full.rotated.jpg'
|
|
684
|
+
if rotated_img_file.exists():
|
|
685
|
+
return rotated_img_file
|
|
686
|
+
|
|
687
|
+
full_img_file = workdir / 'full.jpg'
|
|
688
|
+
if full_img_file.exists():
|
|
689
|
+
return full_img_file
|
|
690
|
+
|
|
691
|
+
return self.filepath
|
|
692
|
+
|
|
693
|
+
def get_full_img(self):
|
|
694
|
+
if self.full_img is not None:
|
|
695
|
+
return self.full_img
|
|
696
|
+
|
|
697
|
+
print('loading full image')
|
|
698
|
+
start = time.time()
|
|
699
|
+
|
|
700
|
+
full_file = self.get_full_file_path()
|
|
701
|
+
self.full_img = cv2.imread(str(full_file))
|
|
702
|
+
|
|
703
|
+
end = time.time()
|
|
704
|
+
print(f'loading image took {end - start} secs')
|
|
705
|
+
|
|
706
|
+
return self.full_img
|
|
707
|
+
|
|
708
|
+
def get_shrunk_img(self):
|
|
709
|
+
if self.small_img is not None:
|
|
710
|
+
return self.small_img
|
|
711
|
+
|
|
712
|
+
workdir = self.get_workdir()
|
|
713
|
+
|
|
714
|
+
small_img_file = workdir.joinpath('small.jpg')
|
|
715
|
+
if small_img_file.exists():
|
|
716
|
+
self.small_img = cv2.imread(str(small_img_file))
|
|
717
|
+
return self.small_img
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
img = self.get_full_img()
|
|
721
|
+
h, w = img.shape[:2]
|
|
722
|
+
|
|
723
|
+
r = self.resize_factor
|
|
724
|
+
if r is None:
|
|
725
|
+
if self.resize_width is not None:
|
|
726
|
+
r = float(self.resize_width) / w
|
|
727
|
+
elif self.resize_height is not None:
|
|
728
|
+
r = float(self.resize_height) / h
|
|
729
|
+
else:
|
|
730
|
+
raise ValueError("Either resize_factor, resize_width or resize_height should be set.")
|
|
731
|
+
|
|
732
|
+
self.ensure_dir(workdir)
|
|
733
|
+
|
|
734
|
+
dim = (int(w*r), int(h*r))
|
|
735
|
+
small_img = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
|
|
736
|
+
cv2.imwrite(str(small_img_file), small_img)
|
|
737
|
+
|
|
738
|
+
# for some reason this fixes some of the issues
|
|
739
|
+
self.small_img = cv2.imread(str(small_img_file))
|
|
740
|
+
return self.small_img
|
|
741
|
+
|
|
742
|
+
def get_biggest_contour(self, img_mask, erode_size, use_bbox_area, max_corner_contour_area_ratio):
|
|
743
|
+
|
|
744
|
+
h, w = img_mask.shape[:2]
|
|
745
|
+
area = h * w
|
|
746
|
+
|
|
747
|
+
if erode_size > 0:
|
|
748
|
+
el1 = cv2.getStructuringElement(cv2.MORPH_RECT, (erode_size, erode_size))
|
|
749
|
+
img_mask = cv2.erode(img_mask, el1)
|
|
750
|
+
elif erode_size < 0:
|
|
751
|
+
el1 = cv2.getStructuringElement(cv2.MORPH_RECT, (-erode_size, -erode_size))
|
|
752
|
+
img_mask = cv2.dilate(img_mask, el1)
|
|
753
|
+
|
|
754
|
+
contours, hierarchy = cv2.findContours(
|
|
755
|
+
img_mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
|
|
756
|
+
)
|
|
757
|
+
ctuples = list(zip(list(contours), list(hierarchy[0])))
|
|
758
|
+
|
|
759
|
+
self.show_contours(img_mask, [x[0] for x in ctuples])
|
|
760
|
+
|
|
761
|
+
ctuples = [ c for c in ctuples if self.get_bbox_area(c) < max_corner_contour_area_ratio*area ]
|
|
762
|
+
|
|
763
|
+
if use_bbox_area:
|
|
764
|
+
ctuples_s = sorted(ctuples, key=self.get_bbox_area, reverse=True)
|
|
765
|
+
map_contour = ctuples_s[0][0]
|
|
766
|
+
else:
|
|
767
|
+
ctuples_s = sorted(ctuples, key=lambda x: cv2.contourArea(x[0]), reverse=True)
|
|
768
|
+
map_inner_contour_idx = ctuples_s[0][1][2]
|
|
769
|
+
map_contour = ctuples[map_inner_contour_idx][0]
|
|
770
|
+
|
|
771
|
+
return map_contour
|
|
772
|
+
|
|
773
|
+
def get_maparea(self):
|
|
774
|
+
workdir = self.get_workdir()
|
|
775
|
+
|
|
776
|
+
maparea_info_file = workdir.joinpath('maparea_info.pkl')
|
|
777
|
+
if maparea_info_file.exists():
|
|
778
|
+
with open(maparea_info_file, 'rb') as f:
|
|
779
|
+
return pickle.load(f)
|
|
780
|
+
|
|
781
|
+
img = self.get_shrunk_img()
|
|
782
|
+
|
|
783
|
+
band_color_choices = self.band_color_choices
|
|
784
|
+
if self.band_color is not None:
|
|
785
|
+
band_color_choices = [ self.band_color ]
|
|
786
|
+
|
|
787
|
+
for band_color in band_color_choices:
|
|
788
|
+
try:
|
|
789
|
+
print(f'getting {band_color} contours')
|
|
790
|
+
img_mask = self.get_color_mask(img, band_color)
|
|
791
|
+
|
|
792
|
+
if self.shrunk_map_area_corners is not None:
|
|
793
|
+
map_contour = np.array(self.shrunk_map_area_corners).reshape((-1,1,2)).astype(np.int32)
|
|
794
|
+
else:
|
|
795
|
+
print(f'getting {band_color} contours for whole image')
|
|
796
|
+
map_contour = self.get_biggest_contour(img_mask, self.collar_erode, self.use_bbox_area, 1.0)
|
|
797
|
+
|
|
798
|
+
self.show_contours(img_mask, [map_contour])
|
|
799
|
+
|
|
800
|
+
map_bbox = cv2.boundingRect(map_contour)
|
|
801
|
+
map_min_rect = cv2.minAreaRect(map_contour)
|
|
802
|
+
map_area = map_bbox[2] * map_bbox[3]
|
|
803
|
+
print(f'{map_bbox=}')
|
|
804
|
+
print(f'{map_min_rect=}')
|
|
805
|
+
print(f'{map_area=}')
|
|
806
|
+
|
|
807
|
+
h, w = img.shape[:2]
|
|
808
|
+
total_area = w * h
|
|
809
|
+
if total_area / map_area > 2:
|
|
810
|
+
raise Exception(f'map area less than expected, {map_area=}, {total_area=}')
|
|
811
|
+
|
|
812
|
+
self.show_contours(img_mask, [map_contour])
|
|
813
|
+
|
|
814
|
+
self.ensure_dir(workdir)
|
|
815
|
+
with open(maparea_info_file, 'wb') as f:
|
|
816
|
+
pickle.dump((map_bbox, map_min_rect, map_contour), f)
|
|
817
|
+
|
|
818
|
+
return map_bbox, map_min_rect, map_contour
|
|
819
|
+
except Exception as e:
|
|
820
|
+
print(f'Error getting map area for {band_color}: {e}')
|
|
821
|
+
|
|
822
|
+
raise Exception(f'no map area found for {self.filepath}, band_color_choices: {band_color_choices}')
|
|
823
|
+
|
|
824
|
+
def rotate(self):
|
|
825
|
+
workdir = self.get_workdir()
|
|
826
|
+
|
|
827
|
+
rotated_info_file = workdir.joinpath('rotated_info.txt')
|
|
828
|
+
if rotated_info_file.exists():
|
|
829
|
+
print('already rotated.. skipping rotation')
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
map_bbox, map_min_rect, _ = self.get_maparea()
|
|
834
|
+
_, _, angle = map_min_rect
|
|
835
|
+
if angle > 45:
|
|
836
|
+
angle = angle - 90
|
|
837
|
+
|
|
838
|
+
self.ensure_dir(workdir)
|
|
839
|
+
|
|
840
|
+
if abs(angle) < self.auto_rotate_thresh:
|
|
841
|
+
print(f'not rotated because angle: {angle}')
|
|
842
|
+
rotated_info_file.write_text(f'{angle}, not_rotated')
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
img = self.get_full_img()
|
|
846
|
+
|
|
847
|
+
print(f'rotating image by {angle}')
|
|
848
|
+
rotated_file = workdir.joinpath('full.rotated.jpg')
|
|
849
|
+
|
|
850
|
+
img_rotated = self.rotate_image_bound(img, -angle)
|
|
851
|
+
cv2.imwrite(str(rotated_file), img_rotated)
|
|
852
|
+
|
|
853
|
+
rotated_info_file.write_text(f'{angle}, rotated')
|
|
854
|
+
|
|
855
|
+
workdir.joinpath('small.jpg').unlink()
|
|
856
|
+
workdir.joinpath('maparea_info.pkl').unlink()
|
|
857
|
+
self.small_img = None
|
|
858
|
+
self.full_img = None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def locate_corners(self, img, corner_overrides):
|
|
862
|
+
|
|
863
|
+
w = img.shape[1]
|
|
864
|
+
h = img.shape[0]
|
|
865
|
+
cw = round(self.corner_ratio * w)
|
|
866
|
+
ch = round(self.corner_ratio * h)
|
|
867
|
+
y = h - 1 - ch
|
|
868
|
+
x = w - 1 - cw
|
|
869
|
+
|
|
870
|
+
print(f'main img dim: {w=}, {h=}')
|
|
871
|
+
# take the four corners
|
|
872
|
+
corner_boxes = []
|
|
873
|
+
corner_boxes.append(((0, 0), (cw, ch)))
|
|
874
|
+
corner_boxes.append(((0, y), (cw, ch)))
|
|
875
|
+
corner_boxes.append(((x, y), (cw, ch)))
|
|
876
|
+
corner_boxes.append(((x, 0), (cw, ch)))
|
|
877
|
+
|
|
878
|
+
directions = [
|
|
879
|
+
(+1,+1),
|
|
880
|
+
(+1,-1),
|
|
881
|
+
(-1,-1),
|
|
882
|
+
(-1,+1),
|
|
883
|
+
]
|
|
884
|
+
anchor_angles = [
|
|
885
|
+
45,
|
|
886
|
+
-45,
|
|
887
|
+
-135,
|
|
888
|
+
135,
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
# get intersection points
|
|
892
|
+
points = []
|
|
893
|
+
for i, corner_box in enumerate(corner_boxes):
|
|
894
|
+
corner_override = corner_overrides[i]
|
|
895
|
+
if corner_override is not None:
|
|
896
|
+
points.append(corner_override)
|
|
897
|
+
continue
|
|
898
|
+
bx, by = corner_box[0]
|
|
899
|
+
bw, bh = corner_box[1]
|
|
900
|
+
c_img = img[by:by+bh, bx:bx+bw]
|
|
901
|
+
print(f'{corner_box=}')
|
|
902
|
+
ipoint = self.get_intersection_point(c_img, directions[i], anchor_angles[i])
|
|
903
|
+
ipoint = bx + ipoint[0], by + ipoint[1]
|
|
904
|
+
points.append(ipoint)
|
|
905
|
+
return points
|
|
906
|
+
|
|
907
|
+
def reorder_poly_points(self, poly_points):
|
|
908
|
+
# sort points in anti clockwise order
|
|
909
|
+
num_corners = len(poly_points)
|
|
910
|
+
box = LinearRing(poly_points + [poly_points[0]])
|
|
911
|
+
if not box.is_ccw:
|
|
912
|
+
poly_points = poly_points.copy()
|
|
913
|
+
poly_points.reverse()
|
|
914
|
+
|
|
915
|
+
center = box.centroid.coords[0]
|
|
916
|
+
#print(center)
|
|
917
|
+
indices = range(0, num_corners)
|
|
918
|
+
indices = [ i for i in indices if poly_points[i][0] < center[0] and poly_points[i][1] < center[1] ]
|
|
919
|
+
def cmp(ci1, ci2):
|
|
920
|
+
c1 = poly_points[ci1]
|
|
921
|
+
c2 = poly_points[ci2]
|
|
922
|
+
if c1[1] == c2[1]:
|
|
923
|
+
return c2[0] - c1[0]
|
|
924
|
+
else:
|
|
925
|
+
return c2[1] - c1[1]
|
|
926
|
+
|
|
927
|
+
s_indices = sorted(indices, key=cmp_to_key(cmp), reverse=True)
|
|
928
|
+
#print(f'{s_indices=}')
|
|
929
|
+
first = s_indices[0]
|
|
930
|
+
poly_reordered = [ poly_points[first] ]
|
|
931
|
+
for i in range(1, num_corners):
|
|
932
|
+
idx = (first - i) % num_corners
|
|
933
|
+
poly_reordered.append(poly_points[idx])
|
|
934
|
+
#print(f'{poly_reordered=}')
|
|
935
|
+
return poly_reordered
|
|
936
|
+
|
|
937
|
+
def remove_text(self, img_mask):
|
|
938
|
+
|
|
939
|
+
if self.text_removal_engine == 'tesseract':
|
|
940
|
+
txt_mask = self.get_text_mask_tesseract(img_mask, self.text_removal_border,
|
|
941
|
+
self.text_removal_iterations,
|
|
942
|
+
self.text_removal_char_size_cutoff,
|
|
943
|
+
self.text_removal_confidence_cutoff,
|
|
944
|
+
self.text_removal_word_char_overlap_size_ratio_cutoff)
|
|
945
|
+
else:
|
|
946
|
+
txt_mask = self.get_text_mask_easyocr(img_mask,
|
|
947
|
+
self.text_removal_border,
|
|
948
|
+
self.text_removal_iterations)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
self.show_image(txt_mask)
|
|
953
|
+
img_mask[txt_mask > 0] = 0
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def get_ext_count(self, point, img_mask, ext_thresh, factor, cwidth):
|
|
957
|
+
x, y = point
|
|
958
|
+
h, w = img_mask.shape[:2]
|
|
959
|
+
uc = 0
|
|
960
|
+
ext_length = 10*factor
|
|
961
|
+
ye = min(y+ext_length, h - 1)
|
|
962
|
+
print(ye)
|
|
963
|
+
uc += np.count_nonzero(img_mask[y:ye, x])
|
|
964
|
+
for i in range(cwidth):
|
|
965
|
+
uc += np.count_nonzero(img_mask[y:ye, x+i])
|
|
966
|
+
uc += np.count_nonzero(img_mask[y:ye, x-i])
|
|
967
|
+
|
|
968
|
+
dc = 0
|
|
969
|
+
ye = max(y-ext_length, 0)
|
|
970
|
+
dc += np.count_nonzero(img_mask[ye:y, x])
|
|
971
|
+
for i in range(cwidth):
|
|
972
|
+
dc += np.count_nonzero(img_mask[ye:y, x+i])
|
|
973
|
+
dc += np.count_nonzero(img_mask[ye:y, x-i])
|
|
974
|
+
|
|
975
|
+
lc = 0
|
|
976
|
+
xe = min(x+ext_length, w - 1)
|
|
977
|
+
lc += np.count_nonzero(img_mask[y, x:xe])
|
|
978
|
+
for i in range(cwidth):
|
|
979
|
+
lc += np.count_nonzero(img_mask[y+i, x:xe])
|
|
980
|
+
lc += np.count_nonzero(img_mask[y-i, x:xe])
|
|
981
|
+
|
|
982
|
+
rc = 0
|
|
983
|
+
xe = max(x-ext_length, 0)
|
|
984
|
+
rc += np.count_nonzero(img_mask[y, xe:x])
|
|
985
|
+
for i in range(cwidth):
|
|
986
|
+
rc += np.count_nonzero(img_mask[y+i, xe:x])
|
|
987
|
+
rc += np.count_nonzero(img_mask[y-i, xe:x])
|
|
988
|
+
|
|
989
|
+
counts = [ uc, dc, rc, lc ]
|
|
990
|
+
print(f'{point=}, {counts=}, {ext_thresh=} {factor=}')
|
|
991
|
+
exts = [ c > ext_thresh*factor*(2*cwidth + 1)/3 for c in counts ]
|
|
992
|
+
return exts.count(True)
|
|
993
|
+
|
|
994
|
+
def get_line_intersections(self, img_mask, find_line_scale, find_line_iter, print_lines=True):
|
|
995
|
+
h, w = img_mask.shape[:2]
|
|
996
|
+
|
|
997
|
+
v_mask, v_lines = self.find_lines(img_mask, direction='vertical', line_scale=find_line_scale, iterations=find_line_iter)
|
|
998
|
+
h_mask, h_lines = self.find_lines(img_mask, direction='horizontal', line_scale=find_line_scale, iterations=find_line_iter)
|
|
999
|
+
if print_lines:
|
|
1000
|
+
print(f'{v_lines=}')
|
|
1001
|
+
print(f'{h_lines=}')
|
|
1002
|
+
|
|
1003
|
+
ips = []
|
|
1004
|
+
only_lines = np.multiply(v_mask, h_mask)
|
|
1005
|
+
jcs, _ = cv2.findContours(only_lines, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
|
|
1006
|
+
for j in jcs:
|
|
1007
|
+
jx, jy, jw, jh = cv2.boundingRect(j)
|
|
1008
|
+
c1, c2 = (2 * jx + jw) // 2, (2 * jy + jh) // 2
|
|
1009
|
+
if 0 < c1 < w - 1 and 0 < c2 < h - 1:
|
|
1010
|
+
ips.append((c1, c2))
|
|
1011
|
+
|
|
1012
|
+
return ips
|
|
1013
|
+
|
|
1014
|
+
def get_nearest_intersection_point(self, img,
|
|
1015
|
+
direction, anchor_angle,
|
|
1016
|
+
line_color, remove_text, expect_band_count,
|
|
1017
|
+
find_line_scale, find_line_iter,
|
|
1018
|
+
corner_max_dist_ratio, corner_min_dist_ratio,
|
|
1019
|
+
min_expected_points, max_corner_angle_diff,
|
|
1020
|
+
max_corner_angle_diff_cutoff):
|
|
1021
|
+
|
|
1022
|
+
img_mask = self.get_color_mask(img, line_color)
|
|
1023
|
+
h, w = img_mask.shape[:2]
|
|
1024
|
+
diag_len = self.get_distance((0,0), (h,w))
|
|
1025
|
+
|
|
1026
|
+
if remove_text:
|
|
1027
|
+
self.remove_text(img_mask)
|
|
1028
|
+
anchor_corner = [
|
|
1029
|
+
0 + w * ( 1 if direction[0] < 0 else 0),
|
|
1030
|
+
0 + h * ( 1 if direction[1] < 0 else 0),
|
|
1031
|
+
]
|
|
1032
|
+
|
|
1033
|
+
self.show_image(img_mask)
|
|
1034
|
+
|
|
1035
|
+
ips = self.get_line_intersections(img_mask, find_line_scale, find_line_iter)
|
|
1036
|
+
print(f'{ips=}')
|
|
1037
|
+
|
|
1038
|
+
self.show_points(ips, img_mask, [255,0,0])
|
|
1039
|
+
|
|
1040
|
+
sorted_ips = sorted(ips, key=lambda p: (self.get_distance(p, anchor_corner), p[0]*direction[0], p[1]*direction[1]))
|
|
1041
|
+
if expect_band_count > 0:
|
|
1042
|
+
anchor_point = sorted_ips[0]
|
|
1043
|
+
diag_len = abs(diag_len - self.get_distance(anchor_corner, anchor_point))
|
|
1044
|
+
self.show_points([anchor_point], img_mask, [0,0,255])
|
|
1045
|
+
remaining = sorted_ips[expect_band_count:]
|
|
1046
|
+
else:
|
|
1047
|
+
remaining = sorted_ips
|
|
1048
|
+
anchor_point = anchor_corner
|
|
1049
|
+
|
|
1050
|
+
angles_before = [ abs(self.get_angle(anchor_point, r) - anchor_angle) for r in remaining ]
|
|
1051
|
+
distances_before = [ self.get_distance(anchor_point, r)/diag_len for r in remaining ]
|
|
1052
|
+
print(f'{angles_before=}')
|
|
1053
|
+
print(f'{distances_before=}')
|
|
1054
|
+
|
|
1055
|
+
remaining = [ r for r in remaining
|
|
1056
|
+
if corner_max_dist_ratio > self.get_distance(anchor_point, r)/diag_len > corner_min_dist_ratio and
|
|
1057
|
+
abs(self.get_angle(anchor_point, r) - anchor_angle) < max_corner_angle_diff_cutoff ]
|
|
1058
|
+
|
|
1059
|
+
if len(remaining) < min_expected_points:
|
|
1060
|
+
raise Exception('too few remaining points')
|
|
1061
|
+
|
|
1062
|
+
angles = [ abs(self.get_angle(anchor_point, r) - anchor_angle) for r in remaining ]
|
|
1063
|
+
distances = [ self.get_distance(anchor_point, r)/diag_len for r in remaining ]
|
|
1064
|
+
print(f'{angles=}')
|
|
1065
|
+
print(f'{distances=}')
|
|
1066
|
+
|
|
1067
|
+
dist_min_index = np.argmin(np.array(distances))
|
|
1068
|
+
|
|
1069
|
+
ip = remaining[dist_min_index]
|
|
1070
|
+
self.show_points([ip], img_mask, [0,255,0])
|
|
1071
|
+
angle = angles[dist_min_index]
|
|
1072
|
+
distance = distances[dist_min_index]
|
|
1073
|
+
if angle > max_corner_angle_diff:
|
|
1074
|
+
raise Exception(f'angle too high: {angle}')
|
|
1075
|
+
print(f'angle: {angle}, distance: {distance}')
|
|
1076
|
+
|
|
1077
|
+
return ip
|
|
1078
|
+
|
|
1079
|
+
def get_4way_intersection_point(self, img, line_color,
|
|
1080
|
+
find_line_scale, find_line_iter,
|
|
1081
|
+
cwidth, ext_thresh):
|
|
1082
|
+
img_mask = self.get_color_mask(img, line_color)
|
|
1083
|
+
|
|
1084
|
+
self.remove_text(img_mask)
|
|
1085
|
+
|
|
1086
|
+
h, w = img_mask.shape[:2]
|
|
1087
|
+
self.show_image(img_mask)
|
|
1088
|
+
|
|
1089
|
+
ips = self.get_line_intersections(img_mask, find_line_scale, find_line_iter)
|
|
1090
|
+
print(f'{ips=}')
|
|
1091
|
+
|
|
1092
|
+
self.show_points(ips, img_mask, [255, 0, 0])
|
|
1093
|
+
|
|
1094
|
+
for pix_factor in [1, 2, 4, 8]:
|
|
1095
|
+
four_corner_ips = []
|
|
1096
|
+
for ip in ips:
|
|
1097
|
+
ext_count = self.get_ext_count(ip, img_mask, ext_thresh, pix_factor, cwidth)
|
|
1098
|
+
print(f'{ip=}, {ext_count}')
|
|
1099
|
+
if ext_count == 4:
|
|
1100
|
+
four_corner_ips.append(ip)
|
|
1101
|
+
|
|
1102
|
+
if len(four_corner_ips) == 0:
|
|
1103
|
+
raise Exception(f'no intersection points found - {len(four_corner_ips)}')
|
|
1104
|
+
|
|
1105
|
+
if len(four_corner_ips) == 1:
|
|
1106
|
+
break
|
|
1107
|
+
|
|
1108
|
+
if len(four_corner_ips) > 1:
|
|
1109
|
+
continue
|
|
1110
|
+
|
|
1111
|
+
self.show_points(four_corner_ips, img_mask, [0, 255, 0])
|
|
1112
|
+
if len(four_corner_ips) > 1:
|
|
1113
|
+
raise Exception(f'multiple intersection points found - {len(four_corner_ips)}')
|
|
1114
|
+
|
|
1115
|
+
return four_corner_ips[0]
|
|
1116
|
+
|
|
1117
|
+
def remove_corner_edges(self, img_mask, direction, remove_corner_edges_ratio):
|
|
1118
|
+
|
|
1119
|
+
h, w = img_mask.shape[:2]
|
|
1120
|
+
print(f'{w=} {h=}')
|
|
1121
|
+
ratio = remove_corner_edges_ratio
|
|
1122
|
+
wdelta = int(w * ratio)
|
|
1123
|
+
hdelta = int(h * ratio)
|
|
1124
|
+
if direction[0] > 0:
|
|
1125
|
+
xrange = [0, wdelta - 1]
|
|
1126
|
+
else:
|
|
1127
|
+
xrange = [w - wdelta + 1, w - 1]
|
|
1128
|
+
|
|
1129
|
+
if direction[1] > 0:
|
|
1130
|
+
yrange = [0, hdelta + 1]
|
|
1131
|
+
else:
|
|
1132
|
+
yrange = [h - hdelta + 1, h - 1]
|
|
1133
|
+
|
|
1134
|
+
print(f'{xrange=} {yrange=}')
|
|
1135
|
+
|
|
1136
|
+
img_mask[yrange[0]:yrange[1], :] = 0
|
|
1137
|
+
img_mask[:, xrange[0]:xrange[1]] = 0
|
|
1138
|
+
|
|
1139
|
+
def get_biggest_contour_corner(self, img,
|
|
1140
|
+
direction, anchor_angle,
|
|
1141
|
+
line_color, remove_text, remove_corner_edges_ratio,
|
|
1142
|
+
corner_erode, max_corner_contour_area_ratio,
|
|
1143
|
+
min_corner_contour_area_ratio,
|
|
1144
|
+
min_corner_dist_ratio, max_corner_angle_diff,
|
|
1145
|
+
pixel_adjustment, picked_corner_max_dist_from_contour_ratio):
|
|
1146
|
+
img_mask = self.get_color_mask(img, line_color)
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
h, w = img_mask.shape[:2]
|
|
1150
|
+
area = h * w
|
|
1151
|
+
diag_len = self.get_distance((0,0), (h,w))
|
|
1152
|
+
|
|
1153
|
+
if remove_text:
|
|
1154
|
+
self.remove_text(img_mask)
|
|
1155
|
+
|
|
1156
|
+
ax = w * (0 if direction[0] > 0 else 1)
|
|
1157
|
+
ay = h * (0 if direction[1] > 0 else 1)
|
|
1158
|
+
|
|
1159
|
+
anchor_corner = (ax, ay)
|
|
1160
|
+
|
|
1161
|
+
self.remove_corner_edges(img_mask, direction, remove_corner_edges_ratio)
|
|
1162
|
+
|
|
1163
|
+
frame_contour = self.get_biggest_contour(img_mask, corner_erode, True, max_corner_contour_area_ratio)
|
|
1164
|
+
|
|
1165
|
+
self.show_contours(img_mask, [frame_contour])
|
|
1166
|
+
|
|
1167
|
+
frame_contour_area = self.get_bbox_area_simple(frame_contour)
|
|
1168
|
+
print(f'{area=}, {frame_contour_area=}, {frame_contour_area/area=}')
|
|
1169
|
+
|
|
1170
|
+
if frame_contour_area < min_corner_contour_area_ratio*area:
|
|
1171
|
+
self.show_contours(img_mask, [frame_contour[0]])
|
|
1172
|
+
raise Exception(f'corner too small, {frame_contour_area/area}')
|
|
1173
|
+
|
|
1174
|
+
bbox = cv2.boundingRect(frame_contour)
|
|
1175
|
+
x_factor = 0 if direction[0] > 0 else 1
|
|
1176
|
+
y_factor = 0 if direction[1] > 0 else 1
|
|
1177
|
+
ip = [bbox[0] + (x_factor*bbox[2]), bbox[1] + (y_factor*bbox[3])]
|
|
1178
|
+
ip = [ ip[0] + pixel_adjustment*direction[0], ip[1] + pixel_adjustment*direction[1] ]
|
|
1179
|
+
self.show_points([ip], img_mask, [255,0,0])
|
|
1180
|
+
|
|
1181
|
+
dist = self.get_distance(anchor_corner, ip)
|
|
1182
|
+
angle = self.get_angle(anchor_corner, ip)
|
|
1183
|
+
dist_ratio = dist/diag_len
|
|
1184
|
+
print(f'{dist_ratio=} {angle=}')
|
|
1185
|
+
if dist_ratio < min_corner_dist_ratio:
|
|
1186
|
+
raise Exception(f'{dist_ratio=} smaller than {min_corner_dist_ratio=}')
|
|
1187
|
+
|
|
1188
|
+
angle_delta = abs(angle - anchor_angle)
|
|
1189
|
+
|
|
1190
|
+
if angle_delta > max_corner_angle_diff:
|
|
1191
|
+
raise Exception(f'{angle_delta=} too big')
|
|
1192
|
+
|
|
1193
|
+
# check if the intersection point is on the contour
|
|
1194
|
+
dist_from_contour = cv2.pointPolygonTest(frame_contour, (ip[0], ip[1]), True)
|
|
1195
|
+
dist_from_contour = -dist_from_contour
|
|
1196
|
+
print(f'{dist_from_contour=}')
|
|
1197
|
+
max_dist = picked_corner_max_dist_from_contour_ratio * diag_len
|
|
1198
|
+
if dist_from_contour > max_dist:
|
|
1199
|
+
raise Exception(f'corner point {ip} too far from contour {dist_from_contour=} > {max_dist=}')
|
|
1200
|
+
elif dist_from_contour > 0:
|
|
1201
|
+
adjustment = dist_from_contour
|
|
1202
|
+
ip = [ ip[0] + adjustment*direction[0], ip[1] + adjustment*direction[1] ]
|
|
1203
|
+
#contour_poly = Polygon(frame_contour.reshape(-1, 2))
|
|
1204
|
+
#picked_point = Point(ip[0], ip[1])
|
|
1205
|
+
#p1, _ = nearest_points(contour_poly, picked_point)
|
|
1206
|
+
#ip = [int(p1.x), int(p1.y)]
|
|
1207
|
+
self.show_points([ip], img_mask, [0,255,0])
|
|
1208
|
+
|
|
1209
|
+
return ip
|
|
1210
|
+
|
|
1211
|
+
def get_nearest_intersection_point_from_biggest_corner_contour(self, img,
|
|
1212
|
+
direction, anchor_angle,
|
|
1213
|
+
line_color, corner_contour_color,
|
|
1214
|
+
remove_corner_edges_ratio, corner_erode,
|
|
1215
|
+
max_corner_contour_area_ratio, min_corner_contour_area_ratio,
|
|
1216
|
+
find_line_scale, find_line_iter,
|
|
1217
|
+
max_corner_angle_diff_cutoff, max_corner_angle_diff,
|
|
1218
|
+
corner_max_dist_ratio):
|
|
1219
|
+
|
|
1220
|
+
h,w = img.shape[:2]
|
|
1221
|
+
area = h * w
|
|
1222
|
+
|
|
1223
|
+
img_c_mask = self.get_color_mask(img, corner_contour_color)
|
|
1224
|
+
self.remove_text(img_c_mask)
|
|
1225
|
+
self.remove_corner_edges(img_c_mask, direction, self.remove_corner_edges_ratio)
|
|
1226
|
+
frame_contour = self.get_biggest_contour(img_c_mask, self.corner_erode, True, max_corner_contour_area_ratio)
|
|
1227
|
+
|
|
1228
|
+
frame_contour_area = self.get_bbox_area_simple(frame_contour)
|
|
1229
|
+
print(f'{area=}, {frame_contour_area=}, {frame_contour_area/area=}')
|
|
1230
|
+
self.show_contours(img_c_mask, [frame_contour])
|
|
1231
|
+
|
|
1232
|
+
if frame_contour_area < min_corner_contour_area_ratio * area:
|
|
1233
|
+
raise Exception(f'corner too small, {frame_contour_area/area}')
|
|
1234
|
+
|
|
1235
|
+
frame_bbox = cv2.boundingRect(frame_contour)
|
|
1236
|
+
fx = frame_bbox[0]
|
|
1237
|
+
fy = frame_bbox[1]
|
|
1238
|
+
fw = frame_bbox[2]
|
|
1239
|
+
fh = frame_bbox[3]
|
|
1240
|
+
|
|
1241
|
+
frame_anchor_point = [
|
|
1242
|
+
fx + fw * ( 1 if direction[0] < 0 else 0),
|
|
1243
|
+
fy + fh * ( 1 if direction[1] < 0 else 0),
|
|
1244
|
+
]
|
|
1245
|
+
frame_diag_len = self.get_distance((0,0), (fh,fw))
|
|
1246
|
+
self.show_points([frame_anchor_point], img_c_mask, [0,0,255])
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
img_mask = self.get_color_mask(img, line_color)
|
|
1250
|
+
#self.remove_text(img_mask)
|
|
1251
|
+
ips = self.get_line_intersections(img_mask, find_line_scale, find_line_iter)
|
|
1252
|
+
ips = [ ip for ip in ips if fx < ip[0] < fx + fw and fy < ip[1] < fy + fh ]
|
|
1253
|
+
self.show_points(ips, img_mask, [255,0,0])
|
|
1254
|
+
|
|
1255
|
+
angles_before = [ abs(self.get_angle(frame_anchor_point, r) - anchor_angle) for r in ips ]
|
|
1256
|
+
distances_before = [ self.get_distance(frame_anchor_point, r)/frame_diag_len for r in ips ]
|
|
1257
|
+
print(f'{angles_before=}')
|
|
1258
|
+
print(f'{distances_before=}')
|
|
1259
|
+
ips = [ ip for ip in ips if abs(self.get_angle(frame_anchor_point, ip) - anchor_angle) < max_corner_angle_diff_cutoff ]
|
|
1260
|
+
|
|
1261
|
+
#sorted_ips = sorted(ips, key=lambda p: (p[0]*direction[0], p[1]*direction[1]))
|
|
1262
|
+
angles = [ abs(self.get_angle(frame_anchor_point, r) - anchor_angle) for r in ips ]
|
|
1263
|
+
distances = [ self.get_distance(frame_anchor_point, r)/frame_diag_len for r in ips ]
|
|
1264
|
+
print(f'{angles=}')
|
|
1265
|
+
print(f'{distances=}')
|
|
1266
|
+
dist_min_index = np.argmin(np.array(distances))
|
|
1267
|
+
distance = distances[dist_min_index]
|
|
1268
|
+
angle = angles[dist_min_index]
|
|
1269
|
+
ip = ips[dist_min_index]
|
|
1270
|
+
if distance > corner_max_dist_ratio or angle > max_corner_angle_diff:
|
|
1271
|
+
raise Exception(f'corner too far or angle too far, {distance=}, {angle=}, {corner_max_dist_ratio=}, {max_corner_angle_diff=}')
|
|
1272
|
+
self.show_points([ip], img_mask, [0,255,0])
|
|
1273
|
+
return ip
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def get_intersection_point(self, img, direction, anchor_angle):
|
|
1277
|
+
raise NotImplementedError("This method should be implemented in a subclass")
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
def locate_corners_generic(self, img, map_poly_points, corner_overrides):
|
|
1281
|
+
w = img.shape[1]
|
|
1282
|
+
#h = img_hsv.shape[0]
|
|
1283
|
+
cw = round(self.corner_ratio * w)
|
|
1284
|
+
#ch = round(corner_ratio * h)
|
|
1285
|
+
|
|
1286
|
+
map_poly_points = self.reorder_poly_points(map_poly_points)
|
|
1287
|
+
print(f'{map_poly_points=}')
|
|
1288
|
+
|
|
1289
|
+
#num_corners = len(map_poly_points)
|
|
1290
|
+
box = LinearRing(map_poly_points + [map_poly_points[0]])
|
|
1291
|
+
buffered_poly = box.buffer(-cw/2, single_sided=True, join_style=JOIN_STYLE.mitre)
|
|
1292
|
+
inner_ring = buffered_poly.interiors[0]
|
|
1293
|
+
corner_centers = list(inner_ring.coords)
|
|
1294
|
+
corner_centers.reverse()
|
|
1295
|
+
print(f'{corner_centers=}')
|
|
1296
|
+
|
|
1297
|
+
corner_boxes = []
|
|
1298
|
+
directions = []
|
|
1299
|
+
anchor_angles = []
|
|
1300
|
+
# get the corner centers and directions
|
|
1301
|
+
angle_map = {
|
|
1302
|
+
(+1,+1): 45,
|
|
1303
|
+
(+1,-1): -45,
|
|
1304
|
+
(-1,-1): -135,
|
|
1305
|
+
(-1,+1): 135
|
|
1306
|
+
}
|
|
1307
|
+
for i,c in enumerate(corner_centers[:-1]):
|
|
1308
|
+
c = (int(c[0]), int(c[1]))
|
|
1309
|
+
p = map_poly_points[i]
|
|
1310
|
+
direction = (+1 if c[0] > p[0] else -1, +1 if c[1] > p[1] else -1)
|
|
1311
|
+
anchor_angle = angle_map[direction]
|
|
1312
|
+
directions.append(direction)
|
|
1313
|
+
anchor_angles.append(anchor_angle)
|
|
1314
|
+
corner_box = ((int(c[0] - cw/2), int(c[1] - cw/2)), (cw, cw))
|
|
1315
|
+
corner_boxes.append(corner_box)
|
|
1316
|
+
|
|
1317
|
+
print(f'{corner_boxes=}')
|
|
1318
|
+
|
|
1319
|
+
points = []
|
|
1320
|
+
for i, corner_box in enumerate(corner_boxes):
|
|
1321
|
+
corner_override = corner_overrides[i]
|
|
1322
|
+
if corner_override is not None:
|
|
1323
|
+
points.append(corner_override)
|
|
1324
|
+
continue
|
|
1325
|
+
print(f'{corner_box=}')
|
|
1326
|
+
bx, by = corner_box[0]
|
|
1327
|
+
bw, bh = corner_box[1]
|
|
1328
|
+
c_img = img[by:by+bh, bx:bx+bw]
|
|
1329
|
+
ipoint = self.get_intersection_point(c_img, directions[i], anchor_angles[i])
|
|
1330
|
+
ipoint = bx + ipoint[0], by + ipoint[1]
|
|
1331
|
+
points.append(ipoint)
|
|
1332
|
+
return points
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def get_poly_points(self, map_contour, poly_approx_factor):
|
|
1337
|
+
perimeter_len = cv2.arcLength(map_contour, True)
|
|
1338
|
+
print(f'{perimeter_len=}')
|
|
1339
|
+
epsilon = poly_approx_factor * perimeter_len
|
|
1340
|
+
map_poly = cv2.approxPolyDP(map_contour, epsilon, True)
|
|
1341
|
+
map_poly_points = [ list(p[0]) for p in map_poly ]
|
|
1342
|
+
return map_poly_points
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def get_corners(self):
|
|
1346
|
+
if self.mapbox_corners is not None:
|
|
1347
|
+
return self.mapbox_corners
|
|
1348
|
+
|
|
1349
|
+
workdir = self.get_workdir()
|
|
1350
|
+
corners_file = workdir.joinpath('corners.json')
|
|
1351
|
+
if corners_file.exists():
|
|
1352
|
+
with open(corners_file, 'r') as f:
|
|
1353
|
+
corners = json.load(f)
|
|
1354
|
+
|
|
1355
|
+
self.mapbox_corners = corners
|
|
1356
|
+
return corners
|
|
1357
|
+
|
|
1358
|
+
map_bbox, _, map_contour = self.get_maparea()
|
|
1359
|
+
map_poly_points = self.get_poly_points(map_contour, self.poly_approx_factor)
|
|
1360
|
+
#if self.extents is None and len(map_poly_points) != 4:
|
|
1361
|
+
# raise Exception(f'extents is None and map_poly_points has {len(map_poly_points)} points, expected 4')
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
full_img = self.get_full_img()
|
|
1365
|
+
small_img = self.get_shrunk_img()
|
|
1366
|
+
|
|
1367
|
+
fh, fw = full_img.shape[:2]
|
|
1368
|
+
sh, sw = small_img.shape[:2]
|
|
1369
|
+
rh, rw = float(fh)/float(sh), float(fw)/float(sw)
|
|
1370
|
+
|
|
1371
|
+
map_bbox_scaled = self.scale_bbox(map_bbox, rw, rh)
|
|
1372
|
+
map_poly_points_scaled = [ self.scale_point(p, rw, rh) for p in map_poly_points ]
|
|
1373
|
+
|
|
1374
|
+
map_img = self.crop_img(full_img, map_bbox_scaled)
|
|
1375
|
+
corner_overrides_full = self.corner_overrides
|
|
1376
|
+
if corner_overrides_full is None:
|
|
1377
|
+
if self.extents is not None:
|
|
1378
|
+
corner_overrides_full = [ None ] * len(map_poly_points)
|
|
1379
|
+
else:
|
|
1380
|
+
corner_overrides_full = [ None ] * 4
|
|
1381
|
+
|
|
1382
|
+
corner_overrides = [ (c[0] - map_bbox_scaled[0], c[1] - map_bbox_scaled[1]) if c is not None else None for c in corner_overrides_full ]
|
|
1383
|
+
map_poly_points_scaled = [ (p[0] - map_bbox_scaled[0], p[1] - map_bbox_scaled[1]) for p in map_poly_points_scaled ]
|
|
1384
|
+
|
|
1385
|
+
if self.extents is None:
|
|
1386
|
+
corners = self.locate_corners(map_img, corner_overrides)
|
|
1387
|
+
else:
|
|
1388
|
+
corners = self.locate_corners_generic(map_img, map_poly_points_scaled, corner_overrides)
|
|
1389
|
+
|
|
1390
|
+
corners = [ (c[0] + map_bbox_scaled[0], c[1] + map_bbox_scaled[1]) for c in corners ]
|
|
1391
|
+
self.mapbox_corners = corners
|
|
1392
|
+
self.ensure_dir(workdir)
|
|
1393
|
+
with open(corners_file, 'w') as f:
|
|
1394
|
+
json.dump(corners, f, indent = 4)
|
|
1395
|
+
|
|
1396
|
+
return corners
|
|
1397
|
+
|
|
1398
|
+
def get_between(self, start, end, divisible_by):
|
|
1399
|
+
"""
|
|
1400
|
+
Get a list of numbers between start and end that are divisible by divisible_by.
|
|
1401
|
+
The numbers are multiplied by factor.
|
|
1402
|
+
"""
|
|
1403
|
+
|
|
1404
|
+
if start % divisible_by != 0:
|
|
1405
|
+
start += divisible_by - (start % divisible_by)
|
|
1406
|
+
if end % divisible_by != 0:
|
|
1407
|
+
end -= end % divisible_by
|
|
1408
|
+
|
|
1409
|
+
return [ (i * divisible_by ) for i in range(start // divisible_by, end // divisible_by + 1) ]
|
|
1410
|
+
|
|
1411
|
+
def correct_intersection(self, point,
|
|
1412
|
+
line_color_choices, context_dim,
|
|
1413
|
+
find_line_scale, find_line_iter):
|
|
1414
|
+
|
|
1415
|
+
ch, cw = context_dim
|
|
1416
|
+
p = (int(point[0]), int(point[1]))
|
|
1417
|
+
full_img = self.get_full_img()
|
|
1418
|
+
# context dim is the size of the image around the intersection point
|
|
1419
|
+
# lets crop to that size
|
|
1420
|
+
bbox = (p[0] - cw, p[1] - ch, cw * 2, ch * 2)
|
|
1421
|
+
img = self.crop_img(full_img, bbox)
|
|
1422
|
+
if img.size == 0 or img.shape[0] == 0 or img.shape[1] == 0:
|
|
1423
|
+
return { 'point': p, 'type': 'invalid' }
|
|
1424
|
+
|
|
1425
|
+
ip = None
|
|
1426
|
+
for line_color in line_color_choices:
|
|
1427
|
+
img_mask = self.get_color_mask(img, line_color)
|
|
1428
|
+
# get the intersection point in the cropped image
|
|
1429
|
+
ips = self.get_line_intersections(img_mask, find_line_scale, find_line_iter, print_lines=False)
|
|
1430
|
+
#self.show_points(ips, img_mask, [255,0,0], radius=5)
|
|
1431
|
+
if len(ips) > 1:
|
|
1432
|
+
return { 'point': point, 'type': 'too_many_choices' }
|
|
1433
|
+
|
|
1434
|
+
if len(ips) == 0:
|
|
1435
|
+
continue
|
|
1436
|
+
|
|
1437
|
+
ip = ips[0]
|
|
1438
|
+
|
|
1439
|
+
if ip is None:
|
|
1440
|
+
return { 'point': point, 'type': 'no_choices' }
|
|
1441
|
+
|
|
1442
|
+
ip = self.translate_point(ip, bbox)
|
|
1443
|
+
|
|
1444
|
+
return { 'point': ip, 'type': 'unchanged' if p == ip else 'corrected' }
|
|
1445
|
+
|
|
1446
|
+
def get_grid_line_corrections(self, points, bounds_check_buffer,
|
|
1447
|
+
line_color_choices, context_dim,
|
|
1448
|
+
find_line_scale, find_line_iter):
|
|
1449
|
+
|
|
1450
|
+
full_pixel_cutline = self.get_full_pixel_cutline()
|
|
1451
|
+
cutline_poly = Polygon([tuple(p) for p in full_pixel_cutline])
|
|
1452
|
+
cutline_poly_buffered = cutline_poly.buffer(bounds_check_buffer, join_style=JOIN_STYLE.mitre)
|
|
1453
|
+
|
|
1454
|
+
corrections = {}
|
|
1455
|
+
|
|
1456
|
+
for p in points:
|
|
1457
|
+
|
|
1458
|
+
if not cutline_poly_buffered.contains(Point(p)):
|
|
1459
|
+
corrections[p] = { 'point': p, 'type': 'invalid' }
|
|
1460
|
+
continue
|
|
1461
|
+
|
|
1462
|
+
correction = self.correct_intersection(p, line_color_choices, context_dim, find_line_scale, find_line_iter)
|
|
1463
|
+
corrections[p] = correction
|
|
1464
|
+
|
|
1465
|
+
return corrections
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def locate_grid_lines_using_trasformer(self, transformer, factor, divisible_by, bounds_check_buffer):
|
|
1470
|
+
|
|
1471
|
+
full_pixel_cutline = self.get_full_pixel_cutline()
|
|
1472
|
+
cutline_poly = Polygon([tuple(p) for p in full_pixel_cutline])
|
|
1473
|
+
cutline_poly_buffered = cutline_poly.buffer(bounds_check_buffer, join_style=JOIN_STYLE.mitre)
|
|
1474
|
+
|
|
1475
|
+
full_pixel_cutline_xy = []
|
|
1476
|
+
for p in full_pixel_cutline:
|
|
1477
|
+
x, y = transformer.xy(p[1], p[0])
|
|
1478
|
+
full_pixel_cutline_xy.append((x, y))
|
|
1479
|
+
|
|
1480
|
+
cutline_poly_xy = Polygon(full_pixel_cutline_xy)
|
|
1481
|
+
|
|
1482
|
+
cutline_poly_xy_bbox = list(cutline_poly_xy.bounds)
|
|
1483
|
+
|
|
1484
|
+
cutline_poly_xy_bbox_rounded_scaled = [ round(c * factor) for c in cutline_poly_xy_bbox ]
|
|
1485
|
+
xmin_scaled = cutline_poly_xy_bbox_rounded_scaled[0]
|
|
1486
|
+
ymin_scaled = cutline_poly_xy_bbox_rounded_scaled[1]
|
|
1487
|
+
xmax_scaled = cutline_poly_xy_bbox_rounded_scaled[2]
|
|
1488
|
+
ymax_scaled = cutline_poly_xy_bbox_rounded_scaled[3]
|
|
1489
|
+
|
|
1490
|
+
print(f'cutline_poly_xy_bbox: {cutline_poly_xy_bbox}')
|
|
1491
|
+
|
|
1492
|
+
xlocs_scaled = self.get_between(xmin_scaled, xmax_scaled, divisible_by)
|
|
1493
|
+
ylocs_scaled = self.get_between(ymin_scaled, ymax_scaled, divisible_by)
|
|
1494
|
+
|
|
1495
|
+
xlocs = [ (x / factor) for x in xlocs_scaled ]
|
|
1496
|
+
ylocs = [ (y / factor) for y in ylocs_scaled ]
|
|
1497
|
+
|
|
1498
|
+
print(f'xlocs: {xlocs}')
|
|
1499
|
+
print(f'ylocs: {ylocs}')
|
|
1500
|
+
|
|
1501
|
+
intersections_scaled = []
|
|
1502
|
+
for x in xlocs_scaled:
|
|
1503
|
+
for y in ylocs_scaled:
|
|
1504
|
+
intersections_scaled.append((x, y))
|
|
1505
|
+
|
|
1506
|
+
lines_xy_scaled = []
|
|
1507
|
+
for inter in intersections_scaled:
|
|
1508
|
+
x, y = inter
|
|
1509
|
+
line1 = (
|
|
1510
|
+
(x, y),
|
|
1511
|
+
(x + divisible_by, y)
|
|
1512
|
+
)
|
|
1513
|
+
line2 = (
|
|
1514
|
+
(x - divisible_by, y),
|
|
1515
|
+
(x, y)
|
|
1516
|
+
)
|
|
1517
|
+
line3 = (
|
|
1518
|
+
(x, y),
|
|
1519
|
+
(x, y + divisible_by)
|
|
1520
|
+
)
|
|
1521
|
+
line4 = (
|
|
1522
|
+
(x, y - divisible_by),
|
|
1523
|
+
(x, y)
|
|
1524
|
+
)
|
|
1525
|
+
lines_xy_scaled += [line1, line2, line3, line4]
|
|
1526
|
+
|
|
1527
|
+
lines_xy = [
|
|
1528
|
+
(
|
|
1529
|
+
(x1 / factor, y1 / factor),
|
|
1530
|
+
(x2 / factor, y2 / factor)
|
|
1531
|
+
)
|
|
1532
|
+
for (x1, y1), (x2, y2) in lines_xy_scaled
|
|
1533
|
+
]
|
|
1534
|
+
lines_xy = set(lines_xy)
|
|
1535
|
+
|
|
1536
|
+
lines = []
|
|
1537
|
+
lines_xy_filtered = []
|
|
1538
|
+
for line in lines_xy:
|
|
1539
|
+
p1 = transformer.rowcol(line[0][0], line[0][1])
|
|
1540
|
+
p2 = transformer.rowcol(line[1][0], line[1][1])
|
|
1541
|
+
point1 = Point(p1)
|
|
1542
|
+
point2 = Point(p2)
|
|
1543
|
+
if cutline_poly_buffered.contains(point1) or cutline_poly_buffered.contains(point2):
|
|
1544
|
+
lines.append((p1, p2))
|
|
1545
|
+
lines_xy_filtered.append(line)
|
|
1546
|
+
|
|
1547
|
+
return lines, lines_xy_filtered
|
|
1548
|
+
|
|
1549
|
+
def locate_grid_lines(self):
|
|
1550
|
+
raise NotImplementedError("This method should be implemented in a subclass")
|
|
1551
|
+
|
|
1552
|
+
def remove_grid_lines(self):
|
|
1553
|
+
workdir = self.get_workdir()
|
|
1554
|
+
|
|
1555
|
+
nogrid_file = workdir.joinpath('nogrid.jpg')
|
|
1556
|
+
if nogrid_file.exists():
|
|
1557
|
+
print(f'{nogrid_file} file exists.. skipping')
|
|
1558
|
+
return
|
|
1559
|
+
|
|
1560
|
+
grid_lines = self.locate_grid_lines()
|
|
1561
|
+
if len(grid_lines) == 0:
|
|
1562
|
+
return
|
|
1563
|
+
|
|
1564
|
+
full_img = self.get_full_img()
|
|
1565
|
+
full_img = full_img.copy()
|
|
1566
|
+
print('dropping grid lines')
|
|
1567
|
+
for line, params in grid_lines:
|
|
1568
|
+
self.remove_line(line, full_img, params)
|
|
1569
|
+
|
|
1570
|
+
self.ensure_dir(workdir)
|
|
1571
|
+
cv2.imwrite(str(nogrid_file), full_img)
|
|
1572
|
+
|
|
1573
|
+
def georeference(self):
|
|
1574
|
+
workdir = self.get_workdir()
|
|
1575
|
+
|
|
1576
|
+
georef_file = workdir.joinpath('georef.tif')
|
|
1577
|
+
final_file = workdir.joinpath('final.tif')
|
|
1578
|
+
if georef_file.exists() or final_file.exists():
|
|
1579
|
+
print(f'{georef_file} or {final_file} exists.. skipping')
|
|
1580
|
+
return
|
|
1581
|
+
|
|
1582
|
+
from_file = self.get_full_file_path()
|
|
1583
|
+
|
|
1584
|
+
if self.should_remove_grid_lines:
|
|
1585
|
+
self.remove_grid_lines()
|
|
1586
|
+
from_file = workdir.joinpath('nogrid.jpg')
|
|
1587
|
+
|
|
1588
|
+
ibox = self.get_sheet_ibox()
|
|
1589
|
+
print(ibox)
|
|
1590
|
+
|
|
1591
|
+
crs_proj = self.get_crs_proj()
|
|
1592
|
+
|
|
1593
|
+
gcps = self.get_gcps()
|
|
1594
|
+
|
|
1595
|
+
gcp_str = ''
|
|
1596
|
+
for gcp in gcps:
|
|
1597
|
+
corner = gcp[0]
|
|
1598
|
+
idx = gcp[1]
|
|
1599
|
+
gcp_str += f' -gcp {corner[0]} {corner[1]} {idx[0]} {idx[1]}'
|
|
1600
|
+
|
|
1601
|
+
creation_options = '-co TILED=YES -co COMPRESS=DEFLATE -co PREDICTOR=2'
|
|
1602
|
+
perf_options = '--config GDAL_CACHEMAX 128 --config GDAL_NUM_THREADS ALL_CPUS'
|
|
1603
|
+
|
|
1604
|
+
translate_cmd = f'gdal_translate {creation_options} {perf_options} {gcp_str} -a_srs "{crs_proj}" -of GTiff {str(from_file)} {str(georef_file)}'
|
|
1605
|
+
self.run_external(translate_cmd)
|
|
1606
|
+
|
|
1607
|
+
def get_cutline_props(self):
|
|
1608
|
+
crs_proj = self.get_crs_proj()
|
|
1609
|
+
|
|
1610
|
+
props = {
|
|
1611
|
+
'id': self.get_id(),
|
|
1612
|
+
'crs': crs_proj,
|
|
1613
|
+
'gcps': self.get_gcps(pre_rotated=True),
|
|
1614
|
+
'pixel_cutline': self.get_full_pixel_cutline(pre_rotated=True),
|
|
1615
|
+
}
|
|
1616
|
+
return props
|
|
1617
|
+
|
|
1618
|
+
def create_cutline(self, ibox, file):
|
|
1619
|
+
cutline_data = {
|
|
1620
|
+
"type": "FeatureCollection",
|
|
1621
|
+
"name": "CUTLINE",
|
|
1622
|
+
"features": [{
|
|
1623
|
+
"type": "Feature",
|
|
1624
|
+
"properties": self.get_cutline_props(),
|
|
1625
|
+
"geometry": {
|
|
1626
|
+
"type": "Polygon",
|
|
1627
|
+
"coordinates": [ibox]
|
|
1628
|
+
}
|
|
1629
|
+
}]
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
self.ensure_dir(file.parent)
|
|
1633
|
+
with open(file, 'w') as f:
|
|
1634
|
+
json.dump(cutline_data, f, indent=4)
|
|
1635
|
+
|
|
1636
|
+
def get_scale(self):
|
|
1637
|
+
raise NotImplementedError("This method should be implemented in a subclass")
|
|
1638
|
+
|
|
1639
|
+
def get_crs_proj(self):
|
|
1640
|
+
raise NotImplementedError("This method should be implemented in a subclass")
|
|
1641
|
+
|
|
1642
|
+
def get_resolution(self):
|
|
1643
|
+
# 1:25000 is 2.1166 for 300 dpi like effect?
|
|
1644
|
+
# TODO: explain the calculation
|
|
1645
|
+
scale = self.get_scale()
|
|
1646
|
+
ratio = scale / 25000
|
|
1647
|
+
return round(2.1166 * ratio, 4)
|
|
1648
|
+
|
|
1649
|
+
def get_full_pixel_cutline(self, pre_rotated=False):
|
|
1650
|
+
|
|
1651
|
+
corners = self.get_corners()
|
|
1652
|
+
corners = corners + [corners[0]] # close the polygon
|
|
1653
|
+
|
|
1654
|
+
corners_poly = Polygon([tuple(p) for p in corners])
|
|
1655
|
+
extra_polys = []
|
|
1656
|
+
for pixel_cutline in self.pixel_cutlines:
|
|
1657
|
+
extra_poly = Polygon([tuple(p) for p in pixel_cutline])
|
|
1658
|
+
extra_polys.append(extra_poly)
|
|
1659
|
+
|
|
1660
|
+
full_poly = unary_union([corners_poly] + extra_polys)
|
|
1661
|
+
if full_poly.geom_type != 'Polygon':
|
|
1662
|
+
for poly in full_poly.geoms:
|
|
1663
|
+
print(f'poly: {poly}')
|
|
1664
|
+
self.save_cutline(full_poly)
|
|
1665
|
+
raise Exception(f'expected full_poly to be a Polygon, got {full_poly.geom_type}')
|
|
1666
|
+
full_poly_points = list(full_poly.exterior.coords)
|
|
1667
|
+
full_poly_points.reverse()
|
|
1668
|
+
full_poly_points = [ (int(p[0]), int(p[1])) for p in full_poly_points ]
|
|
1669
|
+
if pre_rotated:
|
|
1670
|
+
full_poly_points = [ self.get_original_pixel_coordinate((p[0], p[1])) for p in full_poly_points ]
|
|
1671
|
+
|
|
1672
|
+
return full_poly_points
|
|
1673
|
+
|
|
1674
|
+
def get_rotation_reversal_params(self):
|
|
1675
|
+
if self.rotation_reversal_params is not None:
|
|
1676
|
+
return self.rotation_reversal_params
|
|
1677
|
+
|
|
1678
|
+
workdir = self.get_workdir()
|
|
1679
|
+
rotated_info_file = workdir.joinpath('rotated_info.txt')
|
|
1680
|
+
if not rotated_info_file.exists():
|
|
1681
|
+
raise Exception(f'rotated_info.txt does not exist in {workdir}')
|
|
1682
|
+
|
|
1683
|
+
parts = rotated_info_file.read_text().strip().split(',')
|
|
1684
|
+
angle = float(parts[0])
|
|
1685
|
+
rotated = parts[1].strip() == 'rotated'
|
|
1686
|
+
|
|
1687
|
+
if not rotated:
|
|
1688
|
+
self.rotation_reversal_params = RotationReversalParams(False, -angle, None, None)
|
|
1689
|
+
return self.rotation_reversal_params
|
|
1690
|
+
|
|
1691
|
+
rotated_shape = self.get_full_img().shape[:2]
|
|
1692
|
+
orig_file = workdir / 'full.jpg'
|
|
1693
|
+
if not orig_file.exists():
|
|
1694
|
+
orig_file = self.filepath
|
|
1695
|
+
original_shape = cv2.imread(str(orig_file)).shape[:2]
|
|
1696
|
+
self.rotation_reversal_params = RotationReversalParams(True, -angle, rotated_shape, original_shape)
|
|
1697
|
+
|
|
1698
|
+
return self.rotation_reversal_params
|
|
1699
|
+
|
|
1700
|
+
def get_original_pixel_coordinate(self, p):
|
|
1701
|
+
rotation_reversal_params = self.get_rotation_reversal_params()
|
|
1702
|
+
|
|
1703
|
+
if not rotation_reversal_params.rotated:
|
|
1704
|
+
return p
|
|
1705
|
+
|
|
1706
|
+
return self.get_original_pixel_coordinate_internal(p, rotation_reversal_params)
|
|
1707
|
+
|
|
1708
|
+
def get_gcps(self, pre_rotated=False):
|
|
1709
|
+
corners = self.get_corners()
|
|
1710
|
+
if pre_rotated:
|
|
1711
|
+
corners = [ self.get_original_pixel_coordinate((c[0], c[1])) for c in corners ]
|
|
1712
|
+
|
|
1713
|
+
ibox = self.get_sheet_ibox()
|
|
1714
|
+
|
|
1715
|
+
if len(ibox) - 1 != len(corners):
|
|
1716
|
+
raise Exception(f'{len(ibox) - 1=} != {len(corners)=}')
|
|
1717
|
+
|
|
1718
|
+
gcps = []
|
|
1719
|
+
for i, corner in enumerate(corners):
|
|
1720
|
+
idx = ibox[i]
|
|
1721
|
+
gcp = [[corner[0], corner[1]], [idx[0], idx[1]]]
|
|
1722
|
+
gcps.append(gcp)
|
|
1723
|
+
|
|
1724
|
+
return gcps
|
|
1725
|
+
|
|
1726
|
+
def warp_file(self, box, cline_file, georef_file, f_file, jpeg_quality, set_resolution=True):
|
|
1727
|
+
img_quality_config = {
|
|
1728
|
+
'COMPRESS': 'JPEG',
|
|
1729
|
+
#'PHOTOMETRIC': 'YCBCR',
|
|
1730
|
+
'JPEG_QUALITY': f'{jpeg_quality}'
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
crs_proj = self.get_crs_proj()
|
|
1734
|
+
|
|
1735
|
+
self.create_cutline(box, cline_file)
|
|
1736
|
+
|
|
1737
|
+
cutline_options = f'-cutline {str(cline_file)} -cutline_srs "{crs_proj}" -crop_to_cutline --config GDALWARP_IGNORE_BAD_CUTLINE YES -wo CUTLINE_ALL_TOUCHED=TRUE'
|
|
1738
|
+
|
|
1739
|
+
warp_quality_config = img_quality_config.copy()
|
|
1740
|
+
warp_quality_config.update({'TILED': 'YES'})
|
|
1741
|
+
warp_quality_options = ' '.join([ f'-co {k}={v}' for k,v in warp_quality_config.items() ])
|
|
1742
|
+
if not set_resolution:
|
|
1743
|
+
reproj_options = f'-tps -ts 0 {self.warp_output_height} -r bilinear -t_srs "EPSG:3857"'
|
|
1744
|
+
else:
|
|
1745
|
+
res = self.get_resolution()
|
|
1746
|
+
reproj_options = f'-tps -tr {res} {res} -r bilinear -t_srs "EPSG:3857"'
|
|
1747
|
+
#nodata_options = '-dstnodata 0'
|
|
1748
|
+
nodata_options = '-dstalpha'
|
|
1749
|
+
perf_options = '-multi -wo NUM_THREADS=ALL_CPUS --config GDAL_CACHEMAX 1024 -wm 1024'
|
|
1750
|
+
|
|
1751
|
+
warp_cmd = f'gdalwarp -overwrite {perf_options} {nodata_options} {reproj_options} {warp_quality_options} {cutline_options} {str(georef_file)} {str(f_file)}'
|
|
1752
|
+
self.run_external(warp_cmd)
|
|
1753
|
+
|
|
1754
|
+
def get_updated_sheet_ibox(self):
|
|
1755
|
+
if len(self.pixel_cutlines) == 0:
|
|
1756
|
+
return self.get_sheet_ibox()
|
|
1757
|
+
|
|
1758
|
+
gcps = self.get_gcps()
|
|
1759
|
+
transformer = self.get_transformer_from_gcps(gcps)
|
|
1760
|
+
|
|
1761
|
+
sheet_ibox = []
|
|
1762
|
+
full_pixel_cutline = self.get_full_pixel_cutline()
|
|
1763
|
+
for p in full_pixel_cutline:
|
|
1764
|
+
x, y = transformer.xy(p[1], p[0])
|
|
1765
|
+
sheet_ibox.append((x, y))
|
|
1766
|
+
|
|
1767
|
+
return sheet_ibox
|
|
1768
|
+
|
|
1769
|
+
def export_bounds_file(self):
|
|
1770
|
+
bounds_dir = self.get_bounds_dir()
|
|
1771
|
+
|
|
1772
|
+
bounds_file = bounds_dir.joinpath(f'{self.get_id()}.geojsonl')
|
|
1773
|
+
if bounds_file.exists():
|
|
1774
|
+
print(f'{bounds_file} exists.. overwriting')
|
|
1775
|
+
bounds_file.unlink()
|
|
1776
|
+
|
|
1777
|
+
self.ensure_dir(bounds_dir)
|
|
1778
|
+
|
|
1779
|
+
workdir = self.get_workdir()
|
|
1780
|
+
cutline_file = workdir.joinpath('cutline.geojson')
|
|
1781
|
+
crs_proj = self.get_crs_proj()
|
|
1782
|
+
|
|
1783
|
+
self.run_external(f'ogr2ogr -t_srs EPSG:4326 -s_srs "{crs_proj}" -f GeoJSONSeq {str(bounds_file)} {cutline_file}')
|
|
1784
|
+
|
|
1785
|
+
def warp(self):
|
|
1786
|
+
workdir = self.get_workdir()
|
|
1787
|
+
|
|
1788
|
+
cutline_file = workdir.joinpath('cutline.geojson')
|
|
1789
|
+
georef_file = workdir.joinpath('georef.tif')
|
|
1790
|
+
final_file = workdir.joinpath('final.tif')
|
|
1791
|
+
if final_file.exists():
|
|
1792
|
+
print(f'{final_file} exists.. skipping')
|
|
1793
|
+
return
|
|
1794
|
+
|
|
1795
|
+
sheet_ibox = self.get_updated_sheet_ibox()
|
|
1796
|
+
|
|
1797
|
+
self.warp_file(sheet_ibox, cutline_file, georef_file, final_file, self.warp_jpeg_export_quality)
|
|
1798
|
+
|
|
1799
|
+
def export_gtiff(self, filename, out_filename, jpeg_export_quality):
|
|
1800
|
+
if Path(out_filename).exists():
|
|
1801
|
+
print(f'{out_filename} exists.. skipping export')
|
|
1802
|
+
return
|
|
1803
|
+
creation_opts = f'-co TILING_SCHEME=GoogleMapsCompatible -co COMPRESS=JPEG -co QUALITY={jpeg_export_quality}'
|
|
1804
|
+
mask_options = '--config GDAL_TIFF_INTERNAL_MASK YES -b 1 -b 2 -b 3 -mask 4'
|
|
1805
|
+
perf_options = '--config GDAL_CACHEMAX 512'
|
|
1806
|
+
cog_options = '-of COG'
|
|
1807
|
+
cmd = f'gdal_translate {perf_options} {mask_options} {creation_opts} {cog_options} {filename} {out_filename}'
|
|
1808
|
+
self.run_external(cmd)
|
|
1809
|
+
|
|
1810
|
+
def export(self):
|
|
1811
|
+
|
|
1812
|
+
self.export_bounds_file()
|
|
1813
|
+
|
|
1814
|
+
export_file = self.get_export_file()
|
|
1815
|
+
|
|
1816
|
+
self.ensure_dir(export_file.parent)
|
|
1817
|
+
|
|
1818
|
+
final_file = self.get_workdir().joinpath('final.tif')
|
|
1819
|
+
|
|
1820
|
+
self.export_gtiff(str(final_file), str(export_file), self.jpeg_export_quality)
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
def process(self):
|
|
1824
|
+
export_file = self.get_export_file()
|
|
1825
|
+
if export_file.exists():
|
|
1826
|
+
return True
|
|
1827
|
+
|
|
1828
|
+
self.rotate()
|
|
1829
|
+
|
|
1830
|
+
# pause to debug
|
|
1831
|
+
self.prompt1()
|
|
1832
|
+
|
|
1833
|
+
self.georeference()
|
|
1834
|
+
self.warp()
|
|
1835
|
+
self.export()
|
|
1836
|
+
|
|
1837
|
+
# pause to debug
|
|
1838
|
+
self.prompt2()
|
|
1839
|
+
|
|
1840
|
+
# cleanup
|
|
1841
|
+
shutil.rmtree(self.get_workdir(), ignore_errors=True)
|
|
1842
|
+
|
|
1843
|
+
return True
|
|
1844
|
+
|