topo_map_processor 0.1.3__tar.gz → 0.1.5__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.
Files changed (23) hide show
  1. topo_map_processor-0.1.5/PKG-INFO +298 -0
  2. topo_map_processor-0.1.5/README.md +252 -0
  3. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/pyproject.toml +0 -1
  4. topo_map_processor-0.1.5/samples/corner1.png +0 -0
  5. topo_map_processor-0.1.5/samples/corner2.png +0 -0
  6. topo_map_processor-0.1.5/samples/corner3.png +0 -0
  7. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/_version.py +2 -2
  8. topo_map_processor-0.1.5/topo_map_processor/tools/collect_bounds.py +51 -0
  9. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/tools/partition.py +46 -13
  10. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/tools/retile.py +49 -27
  11. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/tools/tile_sources.py +144 -28
  12. topo_map_processor-0.1.3/PKG-INFO +0 -187
  13. topo_map_processor-0.1.3/README.md +0 -141
  14. topo_map_processor-0.1.3/topo_map_processor/tools/collect_bounds.py +0 -23
  15. topo_map_processor-0.1.3/topo_map_processor/tools/update_bounds.py +0 -53
  16. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/.github/workflows/publish.yml +0 -0
  17. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/.gitignore +0 -0
  18. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/.python-version +0 -0
  19. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/LICENSE +0 -0
  20. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/__init__.py +0 -0
  21. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/tools/__init__.py +0 -0
  22. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/topo_map_processor/tools/tile.py +0 -0
  23. {topo_map_processor-0.1.3 → topo_map_processor-0.1.5}/uv.lock +0 -0
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: topo_map_processor
3
+ Version: 0.1.5
4
+ Summary: Library for processing topographic maps
5
+ Project-URL: homepage, https://github.com/ramSeraph/topo_map_processor
6
+ Project-URL: repository, https://github.com/ramSeraph/topo_map_processor.git
7
+ Author-email: Sreeram Kandimalla <kandimalla.sreeram@gmail.com>
8
+ License: This is free and unencumbered software released into the public domain.
9
+
10
+ Anyone is free to copy, modify, publish, use, compile, sell, or
11
+ distribute this software, either in source code form or as a compiled
12
+ binary, for any purpose, commercial or non-commercial, and by any
13
+ means.
14
+
15
+ In jurisdictions that recognize copyright laws, the author or authors
16
+ of this software dedicate any and all copyright interest in the
17
+ software to the public domain. We make this dedication for the benefit
18
+ of the public at large and to the detriment of our heirs and
19
+ successors. We intend this dedication to be an overt act of
20
+ relinquishment in perpetuity of all present and future rights to this
21
+ software under copyright law.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
26
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
27
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
28
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29
+ OTHER DEALINGS IN THE SOFTWARE.
30
+
31
+ For more information, please refer to <https://unlicense.org>
32
+ License-File: LICENSE
33
+ Requires-Python: >=3.12
34
+ Requires-Dist: easyocr>=1.7.2
35
+ Requires-Dist: imgcat>=0.6.0
36
+ Requires-Dist: mercantile>=1.2.1
37
+ Requires-Dist: numpy>=2.3.1
38
+ Requires-Dist: opencv-python-headless>=4.11.0.86
39
+ Requires-Dist: pillow>=11.3.0
40
+ Requires-Dist: pmtiles>=3.4.1
41
+ Requires-Dist: pyproj>=3.7.1
42
+ Requires-Dist: pytesseract>=0.3.13
43
+ Requires-Dist: rasterio>=1.4.3
44
+ Requires-Dist: shapely>=2.1.1
45
+ Description-Content-Type: text/markdown
46
+
47
+ # Topo Map Processor [![PyPI - Latest Version](https://img.shields.io/pypi/v/topo_map_processor)](https://pypi.org/project/topo_map_processor/) [![GitHub Tag](https://img.shields.io/github/v/tag/ramSeraph/topo_map_processor?filter=v*)](https://github.com/ramSeraph/topo_map_processor/releases/latest)
48
+
49
+
50
+ A Python utility for processing topographic maps.
51
+
52
+ ## Description
53
+
54
+ This project provides a collection of command-line tools and a Python library to process, analyze, and manipulate topographic map data, specifically for creating web-mappable tiles from GeoTIFF files. The tools allow you to create tiles, partition large tile sets, and update existing tile sets efficiently.
55
+
56
+ ## Installation
57
+
58
+ It is recommended to use a virtual environment. [uv](https://github.com/astral-sh/uv) is a fast, modern Python package installer and is the recommended way to manage this project.
59
+
60
+ ### For Library or Development
61
+
62
+ If you want to use `topo_map_processor` as a library or contribute to its development, install it from PyPI into a virtual environment.
63
+
64
+ **Using `uv` (Recommended):**
65
+ ```bash
66
+ # Install uv if you don't have it
67
+ pip install uv
68
+
69
+ # Create and activate a virtual environment
70
+ uv venv
71
+ source .venv/bin/activate
72
+
73
+ # Install the package from PyPI
74
+ uv pip install topo_map_processor
75
+ ```
76
+
77
+ **Using `pip`:**
78
+ ```bash
79
+ # Create and activate a virtual environment
80
+ python -m venv .venv
81
+ source .venv/bin/activate
82
+
83
+ # Install the package from PyPI
84
+ pip install topo_map_processor
85
+ ```
86
+
87
+ To contribute, clone the repository and install in editable mode:
88
+ ```bash
89
+ git clone https://github.com/ramSeraph/topo_map_processor.git
90
+ cd topo_map_processor
91
+ uv venv
92
+ source .venv/bin/activate
93
+ uv pip install -e .
94
+ ```
95
+
96
+ ## Usage
97
+
98
+ This project can be used as a library but also provides a set of command-line tools to help with managing the datasets, tiling and creating `pmtiles` files.
99
+
100
+ ### Library Workflow
101
+
102
+ The core of this project is the `TopoMapProcessor` class, which provides a structured workflow for converting a raw scanned topographic map into a Cloud-Optimized GeoTIFF (COG) ready for tiling. The process is designed to be adaptable to different map series by subclassing and implementing key methods.
103
+
104
+ The main steps, executed by the `processor.process()` method, are as follows:
105
+
106
+ 1. **Image Preparation (`rotate`):** To minimize computation, the processor first creates a smaller, shrunken version of the full-resolution map. On this small image, it locates the main contour of the map's frame (the border area). By calculating the angle of this contour's minimum bounding box, it determines the skew of the map. The full-resolution image is then rotated using this angle to make the frame perfectly horizontal, correcting for any tilt introduced during scanning.
107
+ 2. **Corner Detection (`get_corners`):** Once the image is straightened, the processor uses the same map frame contour to identify the four corner regions. By narrowing the search to these smaller areas, it can apply more precise logic to find the exact pixel coordinates of the map's neatline corners. This is a critical step that often requires custom implementation in a subclass (`get_intersection_point`) to handle the specific visual features of the map's corners (e.g., crosses, specific colors, patterns).
108
+ 3. **Grid Line Removal (`remove_grid_lines`):** If configured, the processor can detect and remove grid lines (graticules) from the map image. This is achieved by implementing the `locate_grid_lines` method. The removal helps create a cleaner final map by inpainting the areas where the lines were.
109
+ 4. **Georeferencing (`georeference`):** Using the detected corner coordinates and the corresponding real-world geographic coordinates (provided via the `index_map`), the tool creates Ground Control Points (GCPs). It then uses `gdal_translate` to create an initial georeferenced GeoTIFF.
110
+ 5. **Warping and Cutting (`warp`):** The georeferenced image is then warped into the standard `EPSG:3857` projection used by most web maps. During this step, it's also precisely cropped to the map's boundary (neatline) using a cutline derived from the corner points. The output is a clean, map-only image, correctly projected.
111
+ 6. **Exporting (`export`):** The final warped image is converted into a Cloud-Optimized GeoTIFF (COG), which is highly efficient for web-based tiling. A corresponding bounds file (`.geojsonl`) is also created, defining the geographic extent of the map sheet.
112
+
113
+ The output of this library workflow—a directory of COG `.tif` files and a directory of `.geojsonl` bounds files—serves as the direct input for the command-line tools like `tile`, `collect-bounds`, and `partition`, which handle the final steps of creating a complete, partitioned web map tile set.
114
+
115
+ ### Library Usage
116
+
117
+ The `TopoMapProcessor` class provides a framework for processing individual map sheets. To use it, you need to create a subclass and implement the required methods.
118
+
119
+ Here's an example of how to use the `TopoMapProcessor` class, based on the provided `parse_jica.py` script:
120
+
121
+ ```python
122
+ from topo_map_processor import TopoMapProcessor, LineRemovalParams
123
+
124
+ class SampleProcessor(TopoMapProcessor):
125
+
126
+ def __init__(self, filepath, extra, index_map):
127
+ super().__init__(filepath, extra, index_map)
128
+ # ... (additional initialization)
129
+
130
+ def get_inter_dir(self):
131
+ return Path('data/inter')
132
+
133
+ def get_gtiff_dir(self):
134
+ return Path('export/gtiffs')
135
+
136
+ def get_bounds_dir(self):
137
+ return Path('export/bounds')
138
+
139
+ def get_crs_proj(self):
140
+ return '+proj=tmerc +lat_0=0 +lon_0=84 +k=0.9999 +x_0=500000 +y_0=0 +units=m +ellps=evrst30 +towgs84=293.17,726.18,245.36,0,0,0,0 +no_defs'
141
+
142
+ def get_scale(self):
143
+ return 25000
144
+
145
+ def get_intersection_point(self, img, direction, anchor_angle):
146
+ # ... (implementation for finding intersection points which are the corners of the mapframe)
147
+ # you are expected to pick from the various locate intersection points implementations in the library or write your own
148
+
149
+ def locate_grid_lines(self):
150
+ # ... (implementation for locating grid lines so as to remove them)
151
+ # you are expected to pick from the various locate grid lines implementations in the library or write your own
152
+
153
+ # Example of how to run the processor
154
+ def process_files():
155
+ # ... (load image files and special cases)
156
+ for filepath in image_files:
157
+ extra = special_cases.get(filepath.name, {})
158
+ index_map = get_index_map_jica()
159
+ processor = SampleProcessor(filepath, extra, index_map)
160
+ processor.process()
161
+ ```
162
+
163
+ The library is configurable and most parameters can be overridden at a per sheet level using the `extra` dictionary.
164
+
165
+ The `index_map` is used to map specific file names to their corresponding metadata, which can be useful for processing specific datasets. In particular the index_map is expected to contain the geojson geometry of the map sheet in the `geometry` key, which is used to create the bounds file and georeference the sheet. The geometry is expected to be in counter clockwise order starting at the top left corner of the map sheet.
166
+
167
+ The end result is a Cloud optimized GeoTIFF file, a bounds file in GeoJSON format from which the tiles can be generated, using the `tile` command-line tool, and a set of tiles in the specified directory.
168
+
169
+ I would recommend looking at the [nepal_survey_maps](https://github.com/ramSeraph/nepal_survey_maps) repository for a sample implementation
170
+
171
+ ### Corner Detection Strategies
172
+
173
+ The `get_intersection_point` method is the most critical part to customize for a new map series. The library provides several helper methods that can be used to implement this logic, each suited for different types of corner features:
174
+
175
+ ![corner for nearest intersection point](/samples/corner3.png)
176
+ - **`get_nearest_intersection_point`**: This method is useful when the map corners are defined by the intersection of two perpendicular lines (like a simple crosshair). It works by:
177
+ 1. Detecting all horizontal and vertical lines in the corner region.
178
+ 2. Finding all intersection points between these lines.
179
+ 3. Identifying the point closest to the expected corner position, while also matching an expected angle and distance from an anchor point.
180
+
181
+ ![corner for 4way intersection point](/samples/corner3.png)
182
+ - **`get_4way_intersection_point`**: This is a more robust version for crosshair-style corners. It specifically looks for intersections where four lines meet, making it less prone to errors from stray lines in the map collar.
183
+
184
+ ![corner for biggest contour corner](/samples/corner2.png)
185
+ - **`get_biggest_contour_corner`**: This strategy is designed for corners that are marked by a solid shape or a filled area (e.g., a colored square or circle). It works by:
186
+ 1. Isolating the specific color of the corner feature.
187
+ 2. Finding the largest contour (shape) of that color in the corner region.
188
+ 3. Returning the outermost point of that contour as the corner.
189
+
190
+ ![corner for nearest intersection point from biggest corner contour](/samples/corner1.png)
191
+ - **`get_nearest_intersection_point_from_biggest_corner_contour`**: This is a hybrid approach. It first uses the "biggest contour" method to locate a general corner area (e.g., a colored box) and then searches for a precise line intersection *within* that area. This is effective for maps that have both a colored corner block and a fine crosshair inside it.
192
+
193
+ By combining these methods in your subclass's implementation of `get_intersection_point`, you can build a reliable corner detection system for your specific map type.
194
+
195
+ ### Grid Line Detection Strategies
196
+
197
+ Similar to corner detection, the `locate_grid_lines` method can be implemented using a few key helper methods provided by the library. The primary goal is to identify the precise pixel paths of the map's grid lines (graticules) so they can be removed.
198
+
199
+ - **`locate_grid_lines_using_transformer`**: This is the main strategy for identifying grid lines. It leverages the georeferencing information (the `GCPTransformer`) established from the map corners. The process is as follows:
200
+ 1. It defines a regular grid in the map's real-world coordinate system (e.g., a line every 1000 meters).
201
+ 2. It uses the transformer to project this theoretical grid back onto the pixel space of the image.
202
+ 3. This results in a highly accurate set of lines that represent where the grid lines should be, even accounting for distortions in the map.
203
+
204
+ - **`get_grid_line_corrections`**: This method serves as a refinement step to improve accuracy. While the transformer provides a very close estimate, this function can correct for minor printing or scanning inaccuracies. For each projected grid intersection, it searches a small area in the actual image for the *true* intersection of printed lines, adjusting the point to match the visual data. This ensures that the lines targeted for removal are precisely the ones printed on the map.
205
+
206
+ ### Command-Line Tools
207
+
208
+ This project provides several command-line tools to work with topographic maps. These tools are dynamically loaded, and you can get the most up-to-date usage information by running the tool with the `--help` flag.
209
+
210
+ #### `collect-bounds`
211
+
212
+ Collects individual GeoJSONL bound files into a single GeoJSON FeatureCollection.
213
+
214
+ ```bash
215
+ collect-bounds --bounds-dir <directory> --output-file <file> [--preexisting-file <file>]
216
+ ```
217
+
218
+ - `--bounds-dir`: Directory containing GeoJSONL files. (Required)
219
+ - `--output-file`: Output GeoJSON file path. (Required)
220
+ - `--preexisting-file`: Pre existing GeoJSON file path. (Optional)
221
+
222
+ #### `partition`
223
+
224
+ Partitions a large set of tiles into smaller PMTiles files. This is useful for managing large datasets, for example, to stay within file size limits for services like GitHub Releases. When run with `--only-disk`, it creates a new partitioned `pmtiles` fileset from a directory of tiles. When run without, it can be used to re-partition an existing `pmtiles` fileset, for example after having retiled some of the sheets.
225
+
226
+ ```bash
227
+ partition --to-pmtiles-prefix <prefix> --from-tiles-dir <dir> [--from-pmtiles-prefix <prefix>] [--name <name>] [--description <desc>] [--max-zoom <zoom>] [--min-zoom <zoom>] [--only-disk] (--attribution <text> | --attribution-file <file>)
228
+ ```
229
+
230
+ - `--to-pmtiles-prefix`: Prefix for the output PMTiles files. (Required)
231
+ - `--from-tiles-dir`: Directory containing the input tiles. (Required)
232
+ - `--from-pmtiles-prefix`: Prefix for input PMTiles files (required if not using `--only-disk`).
233
+ - `--name`: Name of the mosaic. (Required if using `--only-disk`)
234
+ - `--description`: Description of the mosaic. (Required if using `--only-disk`)
235
+ - `--max-zoom`: Maximum zoom level for the mosaic. (Defaults to the max zoom found in the source)
236
+ - `--min-zoom`: Minimum zoom level for the mosaic (default: 0).
237
+ - `--only-disk`: Only read tiles from the local disk.
238
+ - `--attribution`: Attribution text for the mosaic. (Required if using `--only-disk`)
239
+ - `--attribution-file`: File containing attribution text. (Required if using `--only-disk`)
240
+
241
+ #### `retile`
242
+
243
+ Retiles a specific list of sheets (GeoTIFFs) and updates the corresponding tiles in an existing tile set. It can determine which other sheets are affected and need to be pulled in to correctly retile the area.
244
+
245
+ The tool works in two stages. First, it calculates the full set of sheets that need to be processed (the "pull list"), which includes the sheets from the input list plus any adjacent sheets that share tiles. You can run the tool with `--sheets-to-pull-list-outfile` to generate this list and then exit. This is useful for fetching the required GeoTIFFs before proceeding.
246
+
247
+ 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.
248
+
249
+ ```bash
250
+ retile --retile-list-file <file> --bounds-file <file> [--max-zoom <zoom>] [--min-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-pmtiles-prefix <prefix>] [--sheets-to-pull-list-outfile <file>]
251
+ ```
252
+
253
+ - `--retile-list-file`: File containing the list of sheets to retile. (Required)
254
+ - `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
255
+ - `--max-zoom`: Maximum zoom level to create tiles for. (Defaults to the max zoom from the PMTiles source)
256
+ - `--min-zoom`: Minimum zoom level to create tiles for. (Defaults to the min zoom from the PMTiles source)
257
+ - `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
258
+ - `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
259
+ - `--from-pmtiles-prefix`: Prefix for the PMTiles source from which to pull existing tiles. (Required if not in pull-list generation mode)
260
+ - `--sheets-to-pull-list-outfile`: If provided, the script will only calculate the full list of sheets that need to be processed (including adjacent ones) and write it to this file, then exit.
261
+
262
+ #### `tile`
263
+
264
+ Creates web map tiles from a directory of GeoTIFF files. It combines them into a virtual raster (`.vrt`) for efficient processing.
265
+
266
+ ```bash
267
+ tile <tiles-dir> <tiffs-dir> <max-zoom> [<min-zoom>]
268
+ ```
269
+
270
+ - `<tiles-dir>`: Directory to store the output tiles. (Required)
271
+ - `<tiffs-dir>`: Directory containing the input GeoTIFF files. (Required)
272
+ - `<max-zoom>`: Maximum zoom level for tiling. (Required)
273
+ - `<min-zoom>`: Minimum zoom level for tiling (default: 0).
274
+
275
+ ### Running Tools Directly with `uvx`
276
+
277
+ You can run the command-line tools directly from PyPI without a persistent installation using `uvx`. This is convenient for one-off tasks.
278
+
279
+ ```bash
280
+
281
+ # Example: Run the 'collect-bounds' tool
282
+ uvx --from topo_map_processor collect-bounds --bounds-dir /path/to/bounds --output-file bounds.geojson
283
+
284
+ # tile and retile have a dependency gdal python library,
285
+ # and some special handling is required so as to install a version of gdal python bindings which matches the version of gdal installed in your system
286
+ # Also numpy and pillow dependencies are required so that the gdal python bindings have some of the resampling features enabled
287
+ # Example: Get help for the 'tile' command
288
+ GDAL_VERSION=$(gdalinfo --version | cut -d"," -f1 | cut -d" " -f2)
289
+ uvx --with numpy --with pillow --with gdal==$GDAL_VERSION --from topo_map_processor tile --help
290
+ ```
291
+
292
+ ## Contributing
293
+
294
+ Contributions are welcome! Please feel free to submit a pull request.
295
+
296
+ ## License
297
+
298
+ This project is under the UNLICENSE
@@ -0,0 +1,252 @@
1
+ # Topo Map Processor [![PyPI - Latest Version](https://img.shields.io/pypi/v/topo_map_processor)](https://pypi.org/project/topo_map_processor/) [![GitHub Tag](https://img.shields.io/github/v/tag/ramSeraph/topo_map_processor?filter=v*)](https://github.com/ramSeraph/topo_map_processor/releases/latest)
2
+
3
+
4
+ A Python utility for processing topographic maps.
5
+
6
+ ## Description
7
+
8
+ This project provides a collection of command-line tools and a Python library to process, analyze, and manipulate topographic map data, specifically for creating web-mappable tiles from GeoTIFF files. The tools allow you to create tiles, partition large tile sets, and update existing tile sets efficiently.
9
+
10
+ ## Installation
11
+
12
+ It is recommended to use a virtual environment. [uv](https://github.com/astral-sh/uv) is a fast, modern Python package installer and is the recommended way to manage this project.
13
+
14
+ ### For Library or Development
15
+
16
+ If you want to use `topo_map_processor` as a library or contribute to its development, install it from PyPI into a virtual environment.
17
+
18
+ **Using `uv` (Recommended):**
19
+ ```bash
20
+ # Install uv if you don't have it
21
+ pip install uv
22
+
23
+ # Create and activate a virtual environment
24
+ uv venv
25
+ source .venv/bin/activate
26
+
27
+ # Install the package from PyPI
28
+ uv pip install topo_map_processor
29
+ ```
30
+
31
+ **Using `pip`:**
32
+ ```bash
33
+ # Create and activate a virtual environment
34
+ python -m venv .venv
35
+ source .venv/bin/activate
36
+
37
+ # Install the package from PyPI
38
+ pip install topo_map_processor
39
+ ```
40
+
41
+ To contribute, clone the repository and install in editable mode:
42
+ ```bash
43
+ git clone https://github.com/ramSeraph/topo_map_processor.git
44
+ cd topo_map_processor
45
+ uv venv
46
+ source .venv/bin/activate
47
+ uv pip install -e .
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ This project can be used as a library but also provides a set of command-line tools to help with managing the datasets, tiling and creating `pmtiles` files.
53
+
54
+ ### Library Workflow
55
+
56
+ The core of this project is the `TopoMapProcessor` class, which provides a structured workflow for converting a raw scanned topographic map into a Cloud-Optimized GeoTIFF (COG) ready for tiling. The process is designed to be adaptable to different map series by subclassing and implementing key methods.
57
+
58
+ The main steps, executed by the `processor.process()` method, are as follows:
59
+
60
+ 1. **Image Preparation (`rotate`):** To minimize computation, the processor first creates a smaller, shrunken version of the full-resolution map. On this small image, it locates the main contour of the map's frame (the border area). By calculating the angle of this contour's minimum bounding box, it determines the skew of the map. The full-resolution image is then rotated using this angle to make the frame perfectly horizontal, correcting for any tilt introduced during scanning.
61
+ 2. **Corner Detection (`get_corners`):** Once the image is straightened, the processor uses the same map frame contour to identify the four corner regions. By narrowing the search to these smaller areas, it can apply more precise logic to find the exact pixel coordinates of the map's neatline corners. This is a critical step that often requires custom implementation in a subclass (`get_intersection_point`) to handle the specific visual features of the map's corners (e.g., crosses, specific colors, patterns).
62
+ 3. **Grid Line Removal (`remove_grid_lines`):** If configured, the processor can detect and remove grid lines (graticules) from the map image. This is achieved by implementing the `locate_grid_lines` method. The removal helps create a cleaner final map by inpainting the areas where the lines were.
63
+ 4. **Georeferencing (`georeference`):** Using the detected corner coordinates and the corresponding real-world geographic coordinates (provided via the `index_map`), the tool creates Ground Control Points (GCPs). It then uses `gdal_translate` to create an initial georeferenced GeoTIFF.
64
+ 5. **Warping and Cutting (`warp`):** The georeferenced image is then warped into the standard `EPSG:3857` projection used by most web maps. During this step, it's also precisely cropped to the map's boundary (neatline) using a cutline derived from the corner points. The output is a clean, map-only image, correctly projected.
65
+ 6. **Exporting (`export`):** The final warped image is converted into a Cloud-Optimized GeoTIFF (COG), which is highly efficient for web-based tiling. A corresponding bounds file (`.geojsonl`) is also created, defining the geographic extent of the map sheet.
66
+
67
+ The output of this library workflow—a directory of COG `.tif` files and a directory of `.geojsonl` bounds files—serves as the direct input for the command-line tools like `tile`, `collect-bounds`, and `partition`, which handle the final steps of creating a complete, partitioned web map tile set.
68
+
69
+ ### Library Usage
70
+
71
+ The `TopoMapProcessor` class provides a framework for processing individual map sheets. To use it, you need to create a subclass and implement the required methods.
72
+
73
+ Here's an example of how to use the `TopoMapProcessor` class, based on the provided `parse_jica.py` script:
74
+
75
+ ```python
76
+ from topo_map_processor import TopoMapProcessor, LineRemovalParams
77
+
78
+ class SampleProcessor(TopoMapProcessor):
79
+
80
+ def __init__(self, filepath, extra, index_map):
81
+ super().__init__(filepath, extra, index_map)
82
+ # ... (additional initialization)
83
+
84
+ def get_inter_dir(self):
85
+ return Path('data/inter')
86
+
87
+ def get_gtiff_dir(self):
88
+ return Path('export/gtiffs')
89
+
90
+ def get_bounds_dir(self):
91
+ return Path('export/bounds')
92
+
93
+ def get_crs_proj(self):
94
+ return '+proj=tmerc +lat_0=0 +lon_0=84 +k=0.9999 +x_0=500000 +y_0=0 +units=m +ellps=evrst30 +towgs84=293.17,726.18,245.36,0,0,0,0 +no_defs'
95
+
96
+ def get_scale(self):
97
+ return 25000
98
+
99
+ def get_intersection_point(self, img, direction, anchor_angle):
100
+ # ... (implementation for finding intersection points which are the corners of the mapframe)
101
+ # you are expected to pick from the various locate intersection points implementations in the library or write your own
102
+
103
+ def locate_grid_lines(self):
104
+ # ... (implementation for locating grid lines so as to remove them)
105
+ # you are expected to pick from the various locate grid lines implementations in the library or write your own
106
+
107
+ # Example of how to run the processor
108
+ def process_files():
109
+ # ... (load image files and special cases)
110
+ for filepath in image_files:
111
+ extra = special_cases.get(filepath.name, {})
112
+ index_map = get_index_map_jica()
113
+ processor = SampleProcessor(filepath, extra, index_map)
114
+ processor.process()
115
+ ```
116
+
117
+ The library is configurable and most parameters can be overridden at a per sheet level using the `extra` dictionary.
118
+
119
+ The `index_map` is used to map specific file names to their corresponding metadata, which can be useful for processing specific datasets. In particular the index_map is expected to contain the geojson geometry of the map sheet in the `geometry` key, which is used to create the bounds file and georeference the sheet. The geometry is expected to be in counter clockwise order starting at the top left corner of the map sheet.
120
+
121
+ The end result is a Cloud optimized GeoTIFF file, a bounds file in GeoJSON format from which the tiles can be generated, using the `tile` command-line tool, and a set of tiles in the specified directory.
122
+
123
+ I would recommend looking at the [nepal_survey_maps](https://github.com/ramSeraph/nepal_survey_maps) repository for a sample implementation
124
+
125
+ ### Corner Detection Strategies
126
+
127
+ The `get_intersection_point` method is the most critical part to customize for a new map series. The library provides several helper methods that can be used to implement this logic, each suited for different types of corner features:
128
+
129
+ ![corner for nearest intersection point](/samples/corner3.png)
130
+ - **`get_nearest_intersection_point`**: This method is useful when the map corners are defined by the intersection of two perpendicular lines (like a simple crosshair). It works by:
131
+ 1. Detecting all horizontal and vertical lines in the corner region.
132
+ 2. Finding all intersection points between these lines.
133
+ 3. Identifying the point closest to the expected corner position, while also matching an expected angle and distance from an anchor point.
134
+
135
+ ![corner for 4way intersection point](/samples/corner3.png)
136
+ - **`get_4way_intersection_point`**: This is a more robust version for crosshair-style corners. It specifically looks for intersections where four lines meet, making it less prone to errors from stray lines in the map collar.
137
+
138
+ ![corner for biggest contour corner](/samples/corner2.png)
139
+ - **`get_biggest_contour_corner`**: This strategy is designed for corners that are marked by a solid shape or a filled area (e.g., a colored square or circle). It works by:
140
+ 1. Isolating the specific color of the corner feature.
141
+ 2. Finding the largest contour (shape) of that color in the corner region.
142
+ 3. Returning the outermost point of that contour as the corner.
143
+
144
+ ![corner for nearest intersection point from biggest corner contour](/samples/corner1.png)
145
+ - **`get_nearest_intersection_point_from_biggest_corner_contour`**: This is a hybrid approach. It first uses the "biggest contour" method to locate a general corner area (e.g., a colored box) and then searches for a precise line intersection *within* that area. This is effective for maps that have both a colored corner block and a fine crosshair inside it.
146
+
147
+ By combining these methods in your subclass's implementation of `get_intersection_point`, you can build a reliable corner detection system for your specific map type.
148
+
149
+ ### Grid Line Detection Strategies
150
+
151
+ Similar to corner detection, the `locate_grid_lines` method can be implemented using a few key helper methods provided by the library. The primary goal is to identify the precise pixel paths of the map's grid lines (graticules) so they can be removed.
152
+
153
+ - **`locate_grid_lines_using_transformer`**: This is the main strategy for identifying grid lines. It leverages the georeferencing information (the `GCPTransformer`) established from the map corners. The process is as follows:
154
+ 1. It defines a regular grid in the map's real-world coordinate system (e.g., a line every 1000 meters).
155
+ 2. It uses the transformer to project this theoretical grid back onto the pixel space of the image.
156
+ 3. This results in a highly accurate set of lines that represent where the grid lines should be, even accounting for distortions in the map.
157
+
158
+ - **`get_grid_line_corrections`**: This method serves as a refinement step to improve accuracy. While the transformer provides a very close estimate, this function can correct for minor printing or scanning inaccuracies. For each projected grid intersection, it searches a small area in the actual image for the *true* intersection of printed lines, adjusting the point to match the visual data. This ensures that the lines targeted for removal are precisely the ones printed on the map.
159
+
160
+ ### Command-Line Tools
161
+
162
+ This project provides several command-line tools to work with topographic maps. These tools are dynamically loaded, and you can get the most up-to-date usage information by running the tool with the `--help` flag.
163
+
164
+ #### `collect-bounds`
165
+
166
+ Collects individual GeoJSONL bound files into a single GeoJSON FeatureCollection.
167
+
168
+ ```bash
169
+ collect-bounds --bounds-dir <directory> --output-file <file> [--preexisting-file <file>]
170
+ ```
171
+
172
+ - `--bounds-dir`: Directory containing GeoJSONL files. (Required)
173
+ - `--output-file`: Output GeoJSON file path. (Required)
174
+ - `--preexisting-file`: Pre existing GeoJSON file path. (Optional)
175
+
176
+ #### `partition`
177
+
178
+ Partitions a large set of tiles into smaller PMTiles files. This is useful for managing large datasets, for example, to stay within file size limits for services like GitHub Releases. When run with `--only-disk`, it creates a new partitioned `pmtiles` fileset from a directory of tiles. When run without, it can be used to re-partition an existing `pmtiles` fileset, for example after having retiled some of the sheets.
179
+
180
+ ```bash
181
+ partition --to-pmtiles-prefix <prefix> --from-tiles-dir <dir> [--from-pmtiles-prefix <prefix>] [--name <name>] [--description <desc>] [--max-zoom <zoom>] [--min-zoom <zoom>] [--only-disk] (--attribution <text> | --attribution-file <file>)
182
+ ```
183
+
184
+ - `--to-pmtiles-prefix`: Prefix for the output PMTiles files. (Required)
185
+ - `--from-tiles-dir`: Directory containing the input tiles. (Required)
186
+ - `--from-pmtiles-prefix`: Prefix for input PMTiles files (required if not using `--only-disk`).
187
+ - `--name`: Name of the mosaic. (Required if using `--only-disk`)
188
+ - `--description`: Description of the mosaic. (Required if using `--only-disk`)
189
+ - `--max-zoom`: Maximum zoom level for the mosaic. (Defaults to the max zoom found in the source)
190
+ - `--min-zoom`: Minimum zoom level for the mosaic (default: 0).
191
+ - `--only-disk`: Only read tiles from the local disk.
192
+ - `--attribution`: Attribution text for the mosaic. (Required if using `--only-disk`)
193
+ - `--attribution-file`: File containing attribution text. (Required if using `--only-disk`)
194
+
195
+ #### `retile`
196
+
197
+ Retiles a specific list of sheets (GeoTIFFs) and updates the corresponding tiles in an existing tile set. It can determine which other sheets are affected and need to be pulled in to correctly retile the area.
198
+
199
+ The tool works in two stages. First, it calculates the full set of sheets that need to be processed (the "pull list"), which includes the sheets from the input list plus any adjacent sheets that share tiles. You can run the tool with `--sheets-to-pull-list-outfile` to generate this list and then exit. This is useful for fetching the required GeoTIFFs before proceeding.
200
+
201
+ 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.
202
+
203
+ ```bash
204
+ retile --retile-list-file <file> --bounds-file <file> [--max-zoom <zoom>] [--min-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-pmtiles-prefix <prefix>] [--sheets-to-pull-list-outfile <file>]
205
+ ```
206
+
207
+ - `--retile-list-file`: File containing the list of sheets to retile. (Required)
208
+ - `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
209
+ - `--max-zoom`: Maximum zoom level to create tiles for. (Defaults to the max zoom from the PMTiles source)
210
+ - `--min-zoom`: Minimum zoom level to create tiles for. (Defaults to the min zoom from the PMTiles source)
211
+ - `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
212
+ - `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
213
+ - `--from-pmtiles-prefix`: Prefix for the PMTiles source from which to pull existing tiles. (Required if not in pull-list generation mode)
214
+ - `--sheets-to-pull-list-outfile`: If provided, the script will only calculate the full list of sheets that need to be processed (including adjacent ones) and write it to this file, then exit.
215
+
216
+ #### `tile`
217
+
218
+ Creates web map tiles from a directory of GeoTIFF files. It combines them into a virtual raster (`.vrt`) for efficient processing.
219
+
220
+ ```bash
221
+ tile <tiles-dir> <tiffs-dir> <max-zoom> [<min-zoom>]
222
+ ```
223
+
224
+ - `<tiles-dir>`: Directory to store the output tiles. (Required)
225
+ - `<tiffs-dir>`: Directory containing the input GeoTIFF files. (Required)
226
+ - `<max-zoom>`: Maximum zoom level for tiling. (Required)
227
+ - `<min-zoom>`: Minimum zoom level for tiling (default: 0).
228
+
229
+ ### Running Tools Directly with `uvx`
230
+
231
+ You can run the command-line tools directly from PyPI without a persistent installation using `uvx`. This is convenient for one-off tasks.
232
+
233
+ ```bash
234
+
235
+ # Example: Run the 'collect-bounds' tool
236
+ uvx --from topo_map_processor collect-bounds --bounds-dir /path/to/bounds --output-file bounds.geojson
237
+
238
+ # tile and retile have a dependency gdal python library,
239
+ # and some special handling is required so as to install a version of gdal python bindings which matches the version of gdal installed in your system
240
+ # Also numpy and pillow dependencies are required so that the gdal python bindings have some of the resampling features enabled
241
+ # Example: Get help for the 'tile' command
242
+ GDAL_VERSION=$(gdalinfo --version | cut -d"," -f1 | cut -d" " -f2)
243
+ uvx --with numpy --with pillow --with gdal==$GDAL_VERSION --from topo_map_processor tile --help
244
+ ```
245
+
246
+ ## Contributing
247
+
248
+ Contributions are welcome! Please feel free to submit a pull request.
249
+
250
+ ## License
251
+
252
+ This project is under the UNLICENSE
@@ -31,7 +31,6 @@ repository = "https://github.com/ramSeraph/topo_map_processor.git"
31
31
 
32
32
  [project.scripts]
33
33
  collect-bounds = "topo_map_processor.tools.collect_bounds:cli"
34
- update-bounds = "topo_map_processor.tools.update_bounds:cli"
35
34
  partition = "topo_map_processor.tools.partition:cli"
36
35
  retile = "topo_map_processor.tools.retile:cli"
37
36
  tile = "topo_map_processor.tools.tile:cli"
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1.3'
21
- __version_tuple__ = version_tuple = (0, 1, 3)
20
+ __version__ = version = '0.1.5'
21
+ __version_tuple__ = version_tuple = (0, 1, 5)
@@ -0,0 +1,51 @@
1
+
2
+ import json
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ def cli():
7
+ parser = argparse.ArgumentParser(description='Collect GeoJSONL files into a single GeoJSON file.')
8
+ parser.add_argument('--bounds-dir', required=True, help='Directory containing GeoJSONL files')
9
+ parser.add_argument('--output-file', required=True, help='Output GeoJSON file path')
10
+ parser.add_argument('--preexisting-file', help='preexisting GeoJSON file to base things on', default=None)
11
+
12
+ args = parser.parse_args()
13
+
14
+ feat_map = {}
15
+ if args.preexisting_file:
16
+ preexisting_file = Path(args.preexisting_file)
17
+ if not preexisting_file.exists():
18
+ raise FileNotFoundError(f"Preexisting file does not exist: {preexisting_file}")
19
+
20
+ existing_data = json.loads(preexisting_file.read_text())
21
+ for feature in existing_data.get('features', []):
22
+ if 'properties' in feature and 'id' in feature['properties']:
23
+ sheet_name = feature['properties']['id']
24
+ feat_map[sheet_name] = feature
25
+
26
+ bounds_dir = Path(args.bounds_dir)
27
+
28
+ paths = list(bounds_dir.glob('*.geojsonl'))
29
+ for p in paths:
30
+ feat = json.loads(p.read_text())
31
+ if 'properties' in feat and 'id' in feat['properties']:
32
+ sheet_name = feat['properties']['id']
33
+ feat_map[sheet_name] = feat
34
+
35
+ with open(args.output_file, 'w') as f:
36
+ f.write('{ "type": "FeatureCollection", "features": [\n')
37
+
38
+ feats = list(feat_map.values())
39
+ for i,feat in enumerate(feats):
40
+ line = json.dumps(feat)
41
+
42
+ if i != len(feats) - 1:
43
+ line += ','
44
+ line += '\n'
45
+ f.write(line)
46
+
47
+ f.write(']}\n')
48
+
49
+ if __name__ == "__main__":
50
+ cli()
51
+