topo_map_processor 0.1.12__tar.gz → 0.1.14__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topo_map_processor
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: Library for processing topographic maps
5
5
  Project-URL: homepage, https://github.com/ramSeraph/topo_map_processor
6
6
  Project-URL: repository, https://github.com/ramSeraph/topo_map_processor.git
@@ -239,11 +239,12 @@ The tool works in two stages. First, it calculates the full set of sheets that n
239
239
  In the second stage, it retiles the affected area. It uses the pull list to create a temporary virtual raster (`.vrt`), generates new base tiles for the affected region, and then reconstructs the upper-level tiles by pulling existing, unaffected tiles from the PMTiles source.
240
240
 
241
241
  ```bash
242
- retile --retile-list-file <file> --bounds-file <file> [--max-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-source <source>] [--sheets-to-pull-list-outfile <file>] [--num-parallel <processes>] [--tile-quality <quality>]
242
+ retile --retile-list-file <file> --bounds-file <file> [--dummy-bounds-file <file>] [--max-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-source <source>] [--sheets-to-pull-list-outfile <file>] [--num-parallel <processes>] [--tile-quality <quality>]
243
243
  ```
244
244
 
245
245
  - `--retile-list-file`: File containing the list of sheets to retile. (Required)
246
246
  - `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
247
+ - `--dummy-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
247
248
  - `--max-zoom`: Maximum zoom level to create tiles for. Defaults to the max zoom from the source if `--from-source` is provided, otherwise it is required.
248
249
  - `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
249
250
  - `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
@@ -190,11 +190,12 @@ The tool works in two stages. First, it calculates the full set of sheets that n
190
190
  In the second stage, it retiles the affected area. It uses the pull list to create a temporary virtual raster (`.vrt`), generates new base tiles for the affected region, and then reconstructs the upper-level tiles by pulling existing, unaffected tiles from the PMTiles source.
191
191
 
192
192
  ```bash
193
- retile --retile-list-file <file> --bounds-file <file> [--max-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-source <source>] [--sheets-to-pull-list-outfile <file>] [--num-parallel <processes>] [--tile-quality <quality>]
193
+ retile --retile-list-file <file> --bounds-file <file> [--dummy-bounds-file <file>] [--max-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-source <source>] [--sheets-to-pull-list-outfile <file>] [--num-parallel <processes>] [--tile-quality <quality>]
194
194
  ```
195
195
 
196
196
  - `--retile-list-file`: File containing the list of sheets to retile. (Required)
197
197
  - `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
198
+ - `--dummy-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
198
199
  - `--max-zoom`: Maximum zoom level to create tiles for. Defaults to the max zoom from the source if `--from-source` is provided, otherwise it is required.
199
200
  - `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
200
201
  - `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.12'
32
- __version_tuple__ = version_tuple = (0, 1, 12)
31
+ __version__ = version = '0.1.14'
32
+ __version_tuple__ = version_tuple = (0, 1, 14)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -85,6 +85,8 @@ class TopoMapProcessor:
85
85
  self.extents = extra.get('extents', None)
86
86
  self.pixel_cutlines = extra.get('pixel_cutlines', [])
87
87
 
88
+ self.inset_pixel_cutlines = extra.get('inset_pixel_cutlines', None)
89
+ self.inset_inpaint_color = extra.get('inset_inpaint_color', (255, 255, 255))
88
90
  # rotation related
89
91
  self.auto_rotate_thresh = extra.get('auto_rotate_thresh', 0.0)
90
92
 
@@ -490,6 +492,10 @@ class TopoMapProcessor:
490
492
  #cv2.imwrite('temp.jpg', img_strip)
491
493
 
492
494
  line_poly_t = translate(line_poly, xoff=-bb[0], yoff=-bb[1])
495
+ # if empty then nothing to do
496
+ if line_poly_t.is_empty:
497
+ print('line poly empty, nothing to do')
498
+ return
493
499
  mask = np.zeros(img_strip.shape[:2], dtype=np.uint8)
494
500
  poly_coords = np.array([ [int(x[0]), int(x[1])] for x in line_poly_t.exterior.coords ])
495
501
  cv2.fillPoly(mask, pts=[poly_coords], color=1)
@@ -767,7 +773,7 @@ class TopoMapProcessor:
767
773
  map_contour = ctuples[map_inner_contour_idx][0]
768
774
 
769
775
  return map_contour
770
-
776
+
771
777
  def get_maparea(self):
772
778
  workdir = self.get_workdir()
773
779
 
@@ -827,7 +833,6 @@ class TopoMapProcessor:
827
833
  print('already rotated.. skipping rotation')
828
834
  return
829
835
 
830
-
831
836
  map_bbox, map_min_rect, _ = self.get_maparea()
832
837
  _, _, angle = map_min_rect
833
838
  if angle > 45:
@@ -1588,9 +1593,6 @@ class TopoMapProcessor:
1588
1593
  self.remove_grid_lines()
1589
1594
  from_file = workdir.joinpath('nogrid.jpg')
1590
1595
 
1591
- ibox = self.get_sheet_ibox()
1592
- print(ibox)
1593
-
1594
1596
  crs_proj = self.get_crs_proj()
1595
1597
 
1596
1598
  gcps = self.get_gcps()
@@ -1604,6 +1606,7 @@ class TopoMapProcessor:
1604
1606
  creation_options = '-co TILED=YES -co COMPRESS=DEFLATE -co PREDICTOR=2'
1605
1607
  perf_options = '--config GDAL_CACHEMAX 128 --config GDAL_NUM_THREADS ALL_CPUS'
1606
1608
 
1609
+ self.ensure_dir(workdir)
1607
1610
  translate_cmd = f'gdal_translate {creation_options} {perf_options} {gcp_str} -a_srs "{crs_proj}" -of GTiff {str(from_file)} {str(georef_file)}'
1608
1611
  self.run_external(translate_cmd)
1609
1612
 
@@ -1749,7 +1752,13 @@ class TopoMapProcessor:
1749
1752
  reproj_options = f'-tps -ts 0 {self.warp_output_height} -r bilinear -t_srs "EPSG:3857"'
1750
1753
  else:
1751
1754
  res = self.get_resolution()
1752
- reproj_options = f'-tps -tr {res} {res} -r bilinear -t_srs "EPSG:3857"'
1755
+ if res == 'auto':
1756
+ reproj_options = '-tps -r bilinear -t_srs "EPSG:3857"'
1757
+ else:
1758
+ if type(res) is not list:
1759
+ res = [res, res]
1760
+ reproj_options = f'-tps -tr {res[0]} {res[1]} -r bilinear -t_srs "EPSG:3857"'
1761
+
1753
1762
  #nodata_options = '-dstnodata 0'
1754
1763
  nodata_options = '-dstalpha'
1755
1764
  perf_options = '-multi -wo NUM_THREADS=ALL_CPUS --config GDAL_CACHEMAX 1024 -wm 1024'
@@ -1772,6 +1781,22 @@ class TopoMapProcessor:
1772
1781
 
1773
1782
  return sheet_ibox
1774
1783
 
1784
+ def inpaint_insets(self, img, inset_pixel_cutlines, inpaint_color):
1785
+ cv_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
1786
+
1787
+ for inset_pixel_cutline in inset_pixel_cutlines:
1788
+ print(f'filling in inset with pixel cutline: {inset_pixel_cutline}')
1789
+ # Create a mask for the inset area
1790
+ mask = np.zeros(cv_img.shape[:2], dtype=np.uint8)
1791
+ pts = np.array([inset_pixel_cutline], dtype=np.int32)
1792
+ cv2.fillPoly(mask, pts, 255)
1793
+ # fill the area in cv_img with inpaint_color
1794
+ cv_img[mask == 255] = inpaint_color
1795
+
1796
+ inpainted_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
1797
+ return Image.fromarray(inpainted_img)
1798
+
1799
+
1775
1800
  def export_bounds_file(self):
1776
1801
  bounds_dir = self.get_bounds_dir()
1777
1802
 
@@ -1800,6 +1825,7 @@ class TopoMapProcessor:
1800
1825
 
1801
1826
  sheet_ibox = self.get_updated_sheet_ibox()
1802
1827
 
1828
+ self.ensure_dir(workdir)
1803
1829
  self.warp_file(sheet_ibox, cutline_file, georef_file, final_file, self.warp_jpeg_export_quality)
1804
1830
 
1805
1831
  def export_gtiff(self, filename, out_filename, jpeg_export_quality):
@@ -1825,12 +1851,24 @@ class TopoMapProcessor:
1825
1851
 
1826
1852
  self.export_gtiff(str(final_file), str(export_file), self.jpeg_export_quality)
1827
1853
 
1854
+ def remove_insets(self):
1855
+ if self.inset_pixel_cutlines is None or len(self.inset_pixel_cutlines) == 0:
1856
+ return
1857
+ full_img = self.get_full_img()
1858
+ inpainted_img = self.inpaint_insets(full_img, self.inset_pixel_cutlines, self.inset_inpaint_color)
1859
+ workdir = self.get_workdir()
1860
+ inpainted_file = workdir.joinpath('full.jpg')
1861
+ self.ensure_dir(workdir)
1862
+ inpainted_img.save(inpainted_file)
1863
+ self.full_img = None
1864
+
1828
1865
 
1829
1866
  def process(self):
1830
1867
  export_file = self.get_export_file()
1831
1868
  if export_file.exists():
1832
1869
  return True
1833
1870
 
1871
+ self.remove_insets()
1834
1872
  self.rotate()
1835
1873
 
1836
1874
  # pause to debug
@@ -4,6 +4,7 @@ import re
4
4
  import sys
5
5
  import json
6
6
  import argparse
7
+ import tempfile
7
8
 
8
9
  import time
9
10
  import subprocess
@@ -24,7 +25,8 @@ from osgeo_utils.gdal2tiles import (
24
25
  DividedCache,
25
26
  )
26
27
 
27
- from pmtiles_mosaic.tile_sources import create_source_from_paths, MissingTileError
28
+ from pmtiles_mosaic.tile_sources import create_source_from_paths
29
+ from pmtiles_mosaic.tiles_common import MissingTileError
28
30
 
29
31
  from .utils import relax_open_file_limit
30
32
 
@@ -84,7 +86,7 @@ class DiskTilesHandler:
84
86
  print(f'deleted {delete_count} files at level {z}')
85
87
 
86
88
  def save_metadata(self, metadata):
87
- metadata_file = self.dir.joinpath('tiles.json')
89
+ metadata_file = self.dir.joinpath('metadata.json')
88
90
  metadata_file.write_text(json.dumps(metadata, indent=2))
89
91
 
90
92
 
@@ -318,7 +320,7 @@ def check_sheets(sheets_to_pull, tiffs_dir):
318
320
  raise Exception(f'missing files {missing}')
319
321
 
320
322
 
321
- def get_sheet_data(bounds_fname):
323
+ def get_sheet_data(bounds_fname, dummy=False):
322
324
 
323
325
  index_file = Path(bounds_fname)
324
326
  if not index_file.exists():
@@ -326,9 +328,14 @@ def get_sheet_data(bounds_fname):
326
328
 
327
329
  index_data = json.loads(index_file.read_text())
328
330
 
331
+ dummy_count = 0
329
332
  sheets_to_box = {}
330
333
  for f in index_data['features']:
331
- sheet_no = f['properties']['id']
334
+ if not dummy:
335
+ sheet_no = f['properties']['id']
336
+ else:
337
+ sheet_no = f'dummy_{dummy_count}'
338
+ dummy_count += 1
332
339
  geom = shape(f['geometry'])
333
340
  xmin, ymin, xmax, ymax = geom.bounds
334
341
  box = mercantile.LngLatBbox(xmin, ymin, xmax, ymax)
@@ -353,33 +360,61 @@ def get_base_tile_sheet_mappings(sheets_to_box, base_zoom):
353
360
 
354
361
 
355
362
 
356
- def assess_sheet_requirements(retile_list_file, bounds_file, max_zoom):
363
+ def assess_sheet_requirements(retile_list_file, bounds_file, dummy_bounds_file, max_zoom):
357
364
 
358
365
  retile_sheets = retile_list_file.read_text().split('\n')
359
366
  retile_sheets = set([ r.strip().replace('.tif', '') for r in retile_sheets if r.strip() != '' ])
360
367
 
361
368
  print('getting base tiles to sheet mapping')
362
369
  sheets_to_box = get_sheet_data(bounds_file)
370
+ dummy_sheets_to_box = {}
371
+ if dummy_bounds_file:
372
+ dummy_sheets_to_box = get_sheet_data(dummy_bounds_file, dummy=True)
373
+ sheets_to_box.update(dummy_sheets_to_box)
374
+
363
375
  sheets_to_base_tiles, base_tiles_to_sheets = get_base_tile_sheet_mappings(sheets_to_box, max_zoom)
364
376
 
365
377
  print('calculating sheets to pull')
366
378
  affected_base_tiles = set()
367
379
  for sheet_no in retile_sheets:
368
380
  affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
381
+ for sheet_no in dummy_sheets_to_box.keys():
382
+ affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
369
383
 
370
384
  sheets_to_pull = set()
371
385
  for tile in affected_base_tiles:
372
386
  to_add = base_tiles_to_sheets[tile]
373
387
  for sheet in to_add:
374
- sheets_to_pull.add(sheet + '.tif')
388
+ if sheet not in dummy_sheets_to_box:
389
+ sheets_to_pull.add(sheet + '.tif')
375
390
 
376
391
  return sheets_to_pull, affected_base_tiles
377
392
 
393
+ def create_dummy_sheets(dummy_bounds_file, tiffs_dir):
394
+ print('creating dummy sheets')
395
+ dummy_sheets_to_box = get_sheet_data(dummy_bounds_file, dummy=True)
396
+
397
+ for sheet_no, box in dummy_sheets_to_box.items():
398
+ file = tiffs_dir.joinpath(f'{sheet_no}.tif')
399
+ if file.exists():
400
+ continue
401
+
402
+ west, south, east, north = box.west, box.south, box.east, box.north
403
+ temp_file = Path(tempfile.mktemp(suffix='.tif'))
404
+ cmd = f'gdal_create -of GTiff -a_srs EPSG:4326 -a_ullr {west} {north} {east} {south} -a_nodata 0 -ot Byte -co COMPRESS=LZW -bands 3 -burn 0 0 0 -outsize 256 256 {temp_file}'
405
+ run_external(cmd)
406
+ temp_file_1 = Path(tempfile.mktemp(suffix='.tif'))
407
+ cmd = f'gdalwarp -dstalpha -t_srs EPSG:3857 -co COMPRESS=LZW {temp_file} {temp_file_1}'
408
+ run_external(cmd)
409
+ cmd = f'gdal_translate -co TILING_SCHEME=GoogleMapsCompatible -co COMPRESS=JPEG -co QUALITY=100 --config GDAL_TIFF_INTERNAL_MASK YES -b 1 -b 2 -b 3 -mask 4 --config GDAL_CACHEMAX 512 -of COG {temp_file_1} {file}'
410
+ run_external(cmd)
411
+
378
412
  def retile_main(args):
379
413
 
380
414
  parser = argparse.ArgumentParser()
381
415
  parser.add_argument('--retile-list-file', required=True, help='File containing list of sheets to retile')
382
416
  parser.add_argument('--bounds-file', required=True, help='Geojson file containing list of available sheets and their georaphic bounds')
417
+ parser.add_argument('--dummy-bounds-file', required=False, help='Geojson file containing georaphic bounds of areas that need to be updated, these areas might not have any new sheets')
383
418
  parser.add_argument('--max-zoom', type=int, help='Maximum zoom level to create tiles for, needed only when --from-source is not provided')
384
419
  parser.add_argument('--sheets-to-pull-list-outfile', default=None,
385
420
  help='Output into which we write the list of sheet that need to be pulled, if set, the script ends after it created the list file')
@@ -399,6 +434,12 @@ def retile_main(args):
399
434
  if not bounds_file.exists():
400
435
  parser.error(f'Bounds file {args.bounds_file} does not exist')
401
436
 
437
+ dummy_bounds_file = None
438
+ if args.dummy_bounds_file:
439
+ dummy_bounds_file = Path(args.dummy_bounds_file)
440
+ if not dummy_bounds_file.exists():
441
+ parser.error(f'Dummy bounds file {args.dummy_bounds_file} does not exist')
442
+
402
443
 
403
444
  if not args.sheets_to_pull_list_outfile:
404
445
  missing = []
@@ -429,7 +470,7 @@ def retile_main(args):
429
470
  print(f'--max-zoom argument is ignored when --from-source is set, using max zoom from original source: {max_zoom}')
430
471
 
431
472
  # calculate the sheets to pull and affected base tiles
432
- sheets_to_pull, affected_base_tiles = assess_sheet_requirements(retile_list_file, bounds_file, max_zoom)
473
+ sheets_to_pull, affected_base_tiles = assess_sheet_requirements(retile_list_file, bounds_file, dummy_bounds_file, max_zoom)
433
474
 
434
475
  if args.sheets_to_pull_list_outfile is not None:
435
476
  Path(args.sheets_to_pull_list_outfile).write_text('\n'.join(sheets_to_pull) + '\n')
@@ -442,6 +483,9 @@ def retile_main(args):
442
483
  print('check the sheets availability')
443
484
  check_sheets(sheets_to_pull, tiffs_dir)
444
485
 
486
+ if dummy_bounds_file is not None:
487
+ create_dummy_sheets(dummy_bounds_file, tiffs_dir)
488
+
445
489
  metadata = orig_source.get_metadata()
446
490
  tile_extension = metadata['format']
447
491
  if tile_extension == 'jpg':
@@ -141,7 +141,7 @@ def cli():
141
141
 
142
142
  vrt_file.unlink()
143
143
 
144
- metadata_file = tiles_dir / 'tiles.json'
144
+ metadata_file = tiles_dir / 'metadata.json'
145
145
 
146
146
  fmt = get_format(args.tile_extension)
147
147