├── .gitignore ├── .gitmodules ├── LICENSE ├── QuadKey ├── .gitignore ├── LICENSE ├── README.md ├── clean ├── quadkey │ ├── __init__.py │ ├── tile_system.py │ └── util.py ├── run_tests.py ├── setup.py └── tests │ ├── __init__.py │ ├── quadkey_tests.py │ ├── tile_system.py │ └── util.py ├── README.md ├── createfinalosm.py ├── createosmanomaly.py ├── findsmallbaseball.py ├── getdatafromosm.py ├── gettilesfrombing.py ├── imagestoosm ├── __init__.py └── config.py ├── import └── phase1 │ ├── reviewed_01.osm │ └── reviewed_02.osm ├── maketrainingimages.py ├── osmmodelconfig.py ├── requirements.txt ├── reviewosmanomaly.py ├── sample-images ├── phase1-baseball-outfieds.png ├── sample1.png ├── sample2.png └── sample3.png ├── train.py ├── train_shapes.py └── trainall.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | anomaly/ 4 | *.py[cod] 5 | *$py.class 6 | all.log 7 | secrets.py 8 | train-images 9 | logs 10 | kernel*.json 11 | 12 | *.csv 13 | *.GeoJSON 14 | *.patch 15 | *.jpeg 16 | *.jpg 17 | *.h5 18 | all.log 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Mask_RCNN"] 2 | path = Mask_RCNN 3 | url = https://github.com/jremillard/Mask_RCNN.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jason Remillard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QuadKey/.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | *.swp 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | __pycache__ 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Virtualenv 41 | venv/ 42 | -------------------------------------------------------------------------------- /QuadKey/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /QuadKey/README.md: -------------------------------------------------------------------------------- 1 | QuadKey 2 | ======= 3 | 4 | Quad key object used for Geospatial segmentation. Based off the idea of a quadtree and used as the Bing Maps tile system. 5 | 6 | Given a (lat, lon) and level produce a quadkey to be used in Bing Maps. 7 | Can also supply methods to generate a Google Maps TileXYZ 8 | 9 | Built off of the TileSystem static class outlined here: http://msdn.microsoft.com/en-us/library/bb259689.aspx 10 | 11 | Converts a lat,lon to pixel space to tile space to a quadkey 12 | 13 | 14 | import quadkey 15 | 16 | qk = quadkey.from_geo((-105, 40), 17) 17 | print qk.key # => 02310101232121212 18 | assert qk.level is 17 19 | tile = qk.to_tile() # => [(x, y), z] 20 | 21 | Not a lot of documentation here, but the implementation has quite a bit, so look at the QuadKey definitions for better documention 22 | 23 | 24 | Install 25 | ------- 26 | 27 | The package on pypi is quadtweet, so the recommended installation is with pip 28 | 29 | pip install quadkey 30 | 31 | Methods 32 | ------- 33 | 34 | There are many straightforward methods, so I'll only go into detail of the unique ones 35 | 36 | * children() 37 | * parent() 38 | * is_ancestor() 39 | * is_descendent() 40 | * area() 41 | * to_geo() 42 | * to_tile() 43 | 44 | ####difference(to) 45 | 46 | Gets the quadkeys between self and to forming a rectangle, inclusive. 47 | 48 | qk.difference(to) -> [qk,,,,,to] 49 | 50 | ####unwind() 51 | 52 | Gets a list of all ancestors in descending order by level, inclusive. 53 | 54 | QuadKey('0123').unwind() -> ['0123','012','01','0'] 55 | -------------------------------------------------------------------------------- /QuadKey/clean: -------------------------------------------------------------------------------- 1 | find . -name "*.pyc" -print0 | xargs -0 rm 2 | -------------------------------------------------------------------------------- /QuadKey/quadkey/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | try: 4 | xrange 5 | except NameError: 6 | xrange = range 7 | 8 | from QuadKey.quadkey.util import precondition 9 | from QuadKey.quadkey.tile_system import TileSystem, valid_key 10 | 11 | LAT_STR = 'lat' 12 | LON_STR = 'lon' 13 | 14 | class QuadKey: 15 | 16 | @precondition(lambda c, key: valid_key(key)) 17 | def __init__(self, key): 18 | """ 19 | A quadkey must be between 1 and 23 digits and can only contain digit[0-3] 20 | """ 21 | self.key = key 22 | self.level = len(key) 23 | 24 | def children(self): 25 | if self.level >= 23: 26 | return [] 27 | return [QuadKey(self.key + str(k)) for k in [0, 1, 2, 3]] 28 | 29 | def parent(self): 30 | return QuadKey(self.key[:-1]) 31 | 32 | def nearby(self): 33 | tile, level = TileSystem.quadkey_to_tile(self.key) 34 | perms = [(-1, -1), (-1, 0), (-1, 1), (0, -1), 35 | (0, 1), (1, -1), (1, 0), (1, 1)] 36 | tiles = set( 37 | map(lambda perm: (abs(tile[0] + perm[0]), abs(tile[1] + perm[1])), perms)) 38 | return [TileSystem.tile_to_quadkey(tile, level) for tile in tiles] 39 | 40 | def is_ancestor(self, node): 41 | """ 42 | If node is ancestor of self 43 | Get the difference in level 44 | If not, None 45 | """ 46 | if self.level <= node.level or self.key[:len(node.key)] != node.key: 47 | return None 48 | return self.level - node.level 49 | 50 | def is_descendent(self, node): 51 | """ 52 | If node is descendent of self 53 | Get the difference in level 54 | If not, None 55 | """ 56 | return node.is_ancestor(self) 57 | 58 | def area(self): 59 | size = TileSystem.map_size(self.level) 60 | LAT = 0 61 | res = TileSystem.ground_resolution(LAT, self.level) 62 | side = (size / 2) * res 63 | return side * side 64 | 65 | def xdifference(self, to): 66 | """ Generator 67 | Gives the difference of quadkeys between self and to 68 | Generator in case done on a low level 69 | Only works with quadkeys of same level 70 | """ 71 | x,y = 0,1 72 | assert self.level == to.level 73 | self_tile = list(self.to_tile()[0]) 74 | to_tile = list(to.to_tile()[0]) 75 | if self_tile[x] >= to_tile[x] and self_tile[y] <= self_tile[y]: 76 | ne_tile, sw_tile = self_tile, to_tile 77 | else: 78 | sw_tile, ne_tile = self_tile, to_tile 79 | cur = ne_tile[:] 80 | while cur[x] >= sw_tile[x]: 81 | while cur[y] <= sw_tile[y]: 82 | yield from_tile(tuple(cur), self.level) 83 | cur[y] += 1 84 | cur[x] -= 1 85 | cur[y] = ne_tile[y] 86 | 87 | def difference(self, to): 88 | """ Non generator version of xdifference 89 | """ 90 | return [qk for qk in self.xdifference(to)] 91 | 92 | def unwind(self): 93 | """ Get a list of all ancestors in descending order of level, including a new instance of self 94 | """ 95 | return [ QuadKey(self.key[:l+1]) for l in reversed(range(len(self.key))) ] 96 | 97 | def to_tile(self): 98 | return TileSystem.quadkey_to_tile(self.key) 99 | 100 | def to_geo(self, centered=False): 101 | ret = TileSystem.quadkey_to_tile(self.key) 102 | tile = ret[0] 103 | lvl = ret[1] 104 | pixel = TileSystem.tile_to_pixel(tile, centered) 105 | return TileSystem.pixel_to_geo(pixel, lvl) 106 | 107 | def __eq__(self, other): 108 | return self.key == other.key 109 | 110 | def __ne__(self, other): 111 | return not self.__eq__(other) 112 | 113 | def __str__(self): 114 | return self.key 115 | 116 | def __repr__(self): 117 | return self.key 118 | 119 | def from_geo(geo, level): 120 | """ 121 | Constucts a quadkey representation from geo and level 122 | geo => (lat, lon) 123 | If lat or lon are outside of bounds, they will be clipped 124 | If level is outside of bounds, an AssertionError is raised 125 | 126 | """ 127 | pixel = TileSystem.geo_to_pixel(geo, level) 128 | tile = TileSystem.pixel_to_tile(pixel) 129 | key = TileSystem.tile_to_quadkey(tile, level) 130 | return QuadKey(key) 131 | 132 | def from_tile(tile, level): 133 | return QuadKey(TileSystem.tile_to_quadkey(tile, level)) 134 | 135 | def from_str(qk_str): 136 | return QuadKey(qk_str) 137 | 138 | def geo_to_dict(geo): 139 | """ Take a geo tuple and return a labeled dict 140 | (lat, lon) -> {'lat': lat, 'lon', lon} 141 | """ 142 | return {LAT_STR: geo[0], LON_STR: geo[1]} 143 | 144 | -------------------------------------------------------------------------------- /QuadKey/quadkey/tile_system.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | try: 4 | xrange 5 | except NameError: 6 | xrange = range 7 | 8 | from QuadKey.quadkey.util import precondition 9 | from math import sin, cos, atan, exp, log, pi 10 | 11 | 12 | def valid_level(level): 13 | LEVEL_RANGE = (1, 23) 14 | return LEVEL_RANGE[0] <= level <= LEVEL_RANGE[1] 15 | 16 | 17 | @precondition(lambda key: valid_level(len(key))) 18 | def valid_key(key): 19 | return TileSystem.KEY_PATTERN.match(key) is not None 20 | 21 | 22 | class TileSystem: 23 | 24 | """ 25 | Class with static method to build quadkeys from lat, lon, levels 26 | see http://msdn.microsoft.com/en-us/library/bb259689.aspx 27 | """ 28 | import re 29 | KEY_PATTERN = re.compile("^[0-3]+$") 30 | 31 | EARTH_RADIUS = 6378137 32 | LATITUDE_RANGE = (-85.05112878, 85.05112878) 33 | LONGITUDE_RANGE = (-180., 180.) 34 | 35 | @staticmethod 36 | @precondition(lambda n, minMax: minMax[0] <= minMax[1]) 37 | def clip(n, minMax): 38 | """ Clips number to specified values """ 39 | return min(max(n, minMax[0]), minMax[1]) 40 | 41 | @staticmethod 42 | @precondition(valid_level) 43 | def map_size(level): 44 | """Determines map height and width in pixel space at level""" 45 | return 256 << level 46 | 47 | @staticmethod 48 | @precondition(lambda lat, lvl: valid_level(lvl)) 49 | def ground_resolution(lat, level): 50 | """Gets ground res in meters / pixel""" 51 | lat = TileSystem.clip(lat, TileSystem.LATITUDE_RANGE) 52 | return cos(lat * pi / 180) * 2 * pi * TileSystem.EARTH_RADIUS / TileSystem.map_size(level) 53 | 54 | @staticmethod 55 | @precondition(lambda lat, lvl, dpi: valid_level(lvl)) 56 | def map_scale(lat, level, dpi): 57 | """Gets the scale of the map expressed as ratio 1 : N. Returns N""" 58 | return TileSystem.ground_resolution(lat, level) * dpi / 0.0254 59 | 60 | @staticmethod 61 | @precondition(lambda geo, lvl: valid_level(lvl)) 62 | def geo_to_pixel(geo, level): 63 | """Transform from geo coordinates to pixel coordinates""" 64 | lat, lon = float(geo[0]), float(geo[1]) 65 | lat = TileSystem.clip(lat, TileSystem.LATITUDE_RANGE) 66 | lon = TileSystem.clip(lon, TileSystem.LONGITUDE_RANGE) 67 | x = (lon + 180) / 360 68 | sin_lat = sin(lat * pi / 180) 69 | y = 0.5 - log((1 + sin_lat) / (1 - sin_lat)) / (4 * pi) 70 | # might need to cast to uint 71 | map_size = TileSystem.map_size(level) 72 | pixel_x = int(TileSystem.clip(x * map_size + 0.5, (0, map_size - 1))) 73 | pixel_y = int(TileSystem.clip(y * map_size + 0.5, (0, map_size - 1))) 74 | # print '\n'+str( ((lat, lon), sin_lat, (x, y), map_size, (pixel_x, 75 | # pixel_y)) )+'\n' 76 | return pixel_x, pixel_y 77 | 78 | @staticmethod 79 | @precondition(lambda pix, lvl: valid_level(lvl)) 80 | def pixel_to_geo(pixel, level): 81 | """Transform from pixel to geo coordinates""" 82 | pixel_x = pixel[0] 83 | pixel_y = pixel[1] 84 | map_size = float(TileSystem.map_size(level)) 85 | x = (TileSystem.clip(pixel_x, (0, map_size - 1)) / map_size) - 0.5 86 | y = 0.5 - (TileSystem.clip(pixel_y, (0, map_size - 1)) / map_size) 87 | lat = 90 - 360 * atan(exp(-y * 2 * pi)) / pi 88 | lon = 360 * x 89 | return round(lat, 6), round(lon, 6) 90 | 91 | @staticmethod 92 | def pixel_to_tile(pixel): 93 | """Transform pixel to tile coordinates""" 94 | return pixel[0] // 256, pixel[1] // 256 95 | 96 | @staticmethod 97 | def tile_to_pixel(tile, centered=False): 98 | """Transform tile to pixel coordinates""" 99 | pixel = [tile[0] * 256, tile[1] * 256] 100 | if centered: 101 | # should clip on max map size 102 | pixel = [pix + 128 for pix in pixel] 103 | return pixel[0], pixel[1] 104 | 105 | @staticmethod 106 | @precondition(lambda tile, lvl: valid_level(lvl)) 107 | def tile_to_quadkey(tile, level): 108 | """Transform tile coordinates to a quadkey""" 109 | tile_x = tile[0] 110 | tile_y = tile[1] 111 | quadkey = "" 112 | for i in xrange(level): 113 | bit = level - i 114 | digit = ord('0') 115 | mask = 1 << (bit - 1) # if (bit - 1) > 0 else 1 >> (bit - 1) 116 | if (tile_x & mask) is not 0: 117 | digit += 1 118 | if (tile_y & mask) is not 0: 119 | digit += 2 120 | quadkey += chr(digit) 121 | return quadkey 122 | 123 | @staticmethod 124 | def quadkey_to_tile(quadkey): 125 | """Transform quadkey to tile coordinates""" 126 | tile_x, tile_y = (0, 0) 127 | level = len(quadkey) 128 | for i in xrange(level): 129 | bit = level - i 130 | mask = 1 << (bit - 1) 131 | if quadkey[level - bit] == '1': 132 | tile_x |= mask 133 | if quadkey[level - bit] == '2': 134 | tile_y |= mask 135 | if quadkey[level - bit] == '3': 136 | tile_x |= mask 137 | tile_y |= mask 138 | return [(tile_x, tile_y), level] 139 | -------------------------------------------------------------------------------- /QuadKey/quadkey/util.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | def condition(precondition=None, postcondition=None): 5 | def decorator(func): 6 | @functools.wraps(func) # preserve name, docstring, etc 7 | def wrapper(*args, **kwargs): # NOTE: no self 8 | if precondition is not None: 9 | assert precondition(*args, **kwargs) 10 | retval = func(*args, **kwargs) # call original function or method 11 | if postcondition is not None: 12 | assert postcondition(retval) 13 | return retval 14 | return wrapper 15 | return decorator 16 | 17 | 18 | def precondition(check): 19 | return condition(precondition=check) 20 | 21 | 22 | def postcondition(check): 23 | return condition(postcondition=check) 24 | -------------------------------------------------------------------------------- /QuadKey/run_tests.py: -------------------------------------------------------------------------------- 1 | import tests 2 | from tests.quadkey_tests import QuadkeyTest 3 | from tests.tile_system import TileSystemTest 4 | from tests.util import UtilTest 5 | 6 | tests.run() 7 | -------------------------------------------------------------------------------- /QuadKey/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup(name="quadkey", 6 | version="0.0.5", 7 | description="Python Implementation for Geospatial Quadkeys", 8 | author="Buck Heroux", 9 | url="https://github.com/buckheroux/QuadKey", 10 | packages=['quadkey'] 11 | ) 12 | -------------------------------------------------------------------------------- /QuadKey/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import TestCase 3 | 4 | if __name__ == '__main__': 5 | unittest.main() 6 | 7 | 8 | def run(): 9 | unittest.main() 10 | -------------------------------------------------------------------------------- /QuadKey/tests/quadkey_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | try: 4 | xrange 5 | except NameError: 6 | xrange = range 7 | 8 | import unittest 9 | from unittest import TestCase 10 | import quadkey 11 | 12 | class QuadkeyTest(TestCase): 13 | 14 | def testInit(self): 15 | qk = quadkey.from_str('0321201120') 16 | with self.assertRaises(AssertionError): 17 | qk = quadkey.from_str('') 18 | with self.assertRaises(AssertionError): 19 | qk = quadkey.from_str('0156510012') 20 | 21 | def testFromGeo(self): 22 | geo = (40, -105) 23 | level = 7 24 | key = quadkey.from_str('0231010') 25 | self.assertEqual(key, quadkey.from_geo(geo, level)) 26 | 27 | def testEquality(self): 28 | one = quadkey.from_str('00') 29 | two = quadkey.from_str('00') 30 | self.assertEqual(one, two) 31 | three = quadkey.from_str('0') 32 | self.assertNotEqual(one, three) 33 | 34 | def testChildren(self): 35 | qk = quadkey.from_str('0') 36 | self.assertEqual( 37 | [c.key for c in qk.children()], ['00', '01', '02', '03']) 38 | qk = quadkey.from_str(''.join(['0' for x in xrange(23)])) 39 | self.assertEqual(qk.children(), []) 40 | 41 | def testAncestry(self): 42 | one = quadkey.from_str('0') 43 | two = quadkey.from_str('0101') 44 | self.assertEqual(3, one.is_descendent(two)) 45 | self.assertIsNone(two.is_descendent(one)) 46 | self.assertEqual(3, two.is_ancestor(one)) 47 | three = quadkey.from_str('1') 48 | self.assertIsNone(three.is_ancestor(one)) 49 | 50 | def testNearby(self): 51 | qk = quadkey.from_str('0') 52 | self.assertEqual(set(['1', '2', '3']), set(qk.nearby())) 53 | #qk = quadkey.from_str('01') 54 | #self.assertEqual( 55 | # set(['00', '10', '02', '03', '13', '33', '32', '23']), set(qk.nearby())) 56 | 57 | def testUnwind(self): 58 | qk = quadkey.from_str('0123') 59 | self.assertEqual( 60 | ['0123', '012', '01', '0'], 61 | [qk.key for qk in qk.unwind()] 62 | ) 63 | 64 | def testDifference(self): 65 | _from = quadkey.from_str('0320101102') 66 | _to = quadkey.from_str('0320101110') 67 | diff = set(['0320101102','0320101100','0320101103', '0320101101', '0320101112', '0320101110']) 68 | self.assertEqual(diff, set([qk.key for qk in _to.difference(_from)])) 69 | self.assertEqual(diff, set([qk.key for qk in _from.difference(_to)])) 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /QuadKey/tests/tile_system.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import TestCase 3 | from quadkey.tile_system import TileSystem 4 | 5 | 6 | class TileSystemTest(TestCase): 7 | 8 | def testClip(self): 9 | self.assertEqual(1, TileSystem.clip(0, (1, 5))) 10 | self.assertEqual(5, TileSystem.clip(10, (1, 5))) 11 | self.assertEqual(3, TileSystem.clip(3, (1, 5))) 12 | with self.assertRaises(AssertionError): 13 | TileSystem.clip(7, (5, 1)) 14 | 15 | def testMapSize(self): 16 | self.assertEqual(512, TileSystem.map_size(1)) 17 | with self.assertRaises(AssertionError): 18 | TileSystem.map_size(0) 19 | 20 | def testGroundResolution(self): 21 | geo = (40., -105.) 22 | res = 936.86657226219847 23 | TileSystem.ground_resolution(geo[0], 7) 24 | 25 | def testMapScale(self): 26 | geo = (40., -105.) 27 | level = 7 28 | dpi = 96 29 | scale = 3540913.029022482 # 3540913.0290224836 ?? 30 | self.assertEqual(scale, TileSystem.map_scale(geo[0], level, dpi)) 31 | 32 | def testGeoToPixel(self): 33 | geo = (40., -105.) 34 | level = 7 35 | pixel = (6827, 12405) 36 | self.assertEqual(pixel, TileSystem.geo_to_pixel(geo, level)) 37 | 38 | def testPixelToGeo(self): 39 | pixel = (6827, 12405) 40 | level = 7 41 | geo = (40.002372, -104.996338) 42 | self.assertEqual(geo, TileSystem.pixel_to_geo(pixel, level)) 43 | 44 | def testPixelToTile(self): 45 | pixel = (6827, 12405) 46 | tile = (26, 48) 47 | self.assertEqual(tile, TileSystem.pixel_to_tile(pixel)) 48 | 49 | def testTileToPixel(self): 50 | tile = (26, 48) 51 | pixel = (6656, 12288) 52 | self.assertEqual(pixel, TileSystem.tile_to_pixel(tile)) 53 | 54 | def testTileToQuadkey(self): 55 | tile = (26, 48) 56 | level = 7 57 | key = "0231010" 58 | self.assertEqual(key, TileSystem.tile_to_quadkey(tile, level)) 59 | 60 | def testQuadkeyToTile(self): 61 | tile = (26, 48) 62 | level = 7 63 | key = "0231010" 64 | self.assertEqual([tile, level], TileSystem.quadkey_to_tile(key)) 65 | -------------------------------------------------------------------------------- /QuadKey/tests/util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import TestCase 3 | from quadkey.util import * 4 | 5 | 6 | class UtilTest(TestCase): 7 | 8 | def testPrecondition(self): 9 | self.assertTrue(self.pre(True)) 10 | with self.assertRaises(AssertionError): 11 | self.pre(False) 12 | 13 | def testPostcondition(self): 14 | pass 15 | 16 | @precondition(lambda c, x: x is True) 17 | def pre(self, x): 18 | return x 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Images to OSM 2 | This project uses the Mask R-CNN algorithm to detect features in satellite images. The goal is to test the Mask R-CNN neural network algorithm and improve OpenStreetMap by adding high quality baseball, soccer, tennis, football, and basketball fields to the map. 3 | 4 | The [Mask R-CNN]([https://arxiv.org/abs/1703.06870) was published March 2017, by the [Facebook AI Research (FAIR)](https://research.fb.com/category/facebook-ai-research-fair/). 5 | 6 | This paper claims state of the art performance for detecting instance segmentation masks. The paper is an exciting result because "solving" the instance segmentation mask problem will benefit numerious practical applications outside of Facebook and OpenStreetMap. 7 | 8 | Using Mask R-CNN successfully on a new data set would be a good indication that the algorithm is generic enough to be applicable on many problems. However, the number of publicly available data sets with enough images to train this algorithm are limited because collecting and annotating data for 50,000+ images is expensive and time consuming. 9 | 10 | Microsoft's Bing satellite tiles, combined with the OpenStreetMap data, is a good source of segmentation mask data. The opportunity of working with a cutting edge AI algorithms and doing my favorite hobby (OSM) was too much to pass up. 11 | 12 | ## Samples Images 13 | 14 | Mask R-CNN finding baseball, basketball, and tennis fields in Bing images. 15 | 16 | ![OSM Mask R-CNN sample 1](/sample-images/sample1.png) 17 | ![OSM Mask R-CNN sample 2](/sample-images/sample2.png) 18 | ![OSM Mask R-CNN sample 3](/sample-images/sample3.png) 19 | 20 | ## Mask R-CNN Implementation 21 | 22 | At this time (end of 2017), Facebook AI research has not yet released their implementation. [Matterport, Inc](https://matterport.com/) has graciously released a very nice python [implementation of Mask R-CNN](https://github.com/matterport/Mask_RCNN) on github using Keras and TensorFlow. This project is based on Matterport, Inc work. 23 | 24 | ## Why Sports Fields 25 | 26 | Sport fields are a good fit for the Mask R-CNN algorithm. 27 | 28 | - They are visible in the satellite images regardless of the tree cover, unlike, say, buildings. 29 | - They are "blob" shape and not a line shape, like a roads. 30 | - If successful, they are easy to conflate and import back into OSM, because they are isolated features. 31 | 32 | ## Training with OSM 33 | 34 | The stretch goal for this project is to train a neural network at human level performance and to completely map the sports fields in Massachusetts in OSM. Unfortunately the existing data in OSM is not of high enough quality to train any algorithm to human level performance. The plan is to iteratively train, feed corrections back to OSM, and re-train, bootstrapping the algorithm and OSM together. Hopefully a virtuous circle between OSM and the algorithm will form until the algorithm is good as a human mapper. 35 | 36 | ## Workflow 37 | 38 | The training workflow is in the trainall.py, which calls the following scripts in sequence. 39 | 40 | 1. getdatafromosm.py uses overpass to download the data for the sports fields. 41 | 2. gettilesfrombing.py uses the OSM data to download the required Bing tiles. The script downloads the data slowly, please expect around 2 days to run the first time. 42 | 3. maketrainingimages.py collects the OSM data, and the Bing tiles into a set of training images and masks. Expect 12 hours to run each time. 43 | 4. train.py actually runs training for the Mask R-CNN algorithm. Expect that this will take 4 days to run on single GTX 1080 with 8GB of memory. 44 | 45 | ## Convert Results to OSM File 46 | 47 | 5. createosmanomaly.py runs the neural network over the training image set and suggests changes to OSM. 48 | 49 | This script converts the neural network output masks into the candidate OSM ways. It does this by fitting perfect rectangles to tennis and basketball mask boundaries. For baseball fields, the OSM ways are a fitted 90 degree wedges and the simplified masks boundary. The mask fitting is a nonlinear optimization problem and it is performed with a simplex optimizer using a robust Huber cost function. The simplex optimizer was used because I was too lazy code a partial derivative function. The boundary being fit is not a gaussian process, therefor the Huber cost function is a better choice than a standard least squared cost function. The unknown rotation of the features causes the fitting optimization to be highly non-convex. In English, the optimization gets stuck in local valleys if it is started far away from the optimal solution. This is handled by simply seeding the optimizer at several rotations and emitting all the high quality fits. A human using the reviewosmanomaly.py script sorts out which rotation is the right one. Hopefully as the neural network performance on baseball fields improves the alternate rotations can be removed. 50 | 51 | In order to hit the stretch goal, the training data from OSM will need to be pristine. The script will need to be extended to identify incorrectly tagged fields and fields that are poorly traced. For now, it simply identifies fields that are missing from OSM. 52 | 53 | 6. The reviewosmanomaly.py is run next to visually approve or reject the changes suggested in the anomaly directory. 54 | 55 | Note this is the only script that requires user interaction. The script clusters together suggestions from createosmanomaly.py and presents an gallery options. The the user visually inspects the image gallery and approves or reject changes suggested by createosmanomaly.py. The images shown are of the final way geometry over the Bing satellite images. 56 | 57 | 7. The createfinalosm.py creates the final .osm files from the anomaly review done by reviewosmanomaly.py. It breaks up the files so that the final OSM file size is under the 10,000 element limit of the OSM API. 58 | 59 | ## Phase 1 - Notes ## 60 | 61 | Phase 1 of the project is training the neural network directly off of the unimproved OSM data, and [importing missing fields](https://wiki.openstreetmap.org/wiki/US_Sports_Fields_Import_2018) from the training images back into OSM. About 2,800 missing fields were identified and will soon be imported 62 | back into OSM. 63 | 64 | For tennis and basketball courts the performance is quite good. The masks are rectangles with few 65 | false positives. Like a human mapper it has no problem handling clusters of tennis and basketball courts, rotations, occlusions from trees, and different colored pavement. It is close, but not quite at human performance. After the missing fields are imported into OSM, hopefully it will reach human level performance. 66 | 67 | The good news/bad news are the baseball fields. They are much more challenging and interesting than the tennis and basketball courts. First off, they have a large variation in scale. A baseball field for very small children is 5x to 6x smaller than a full sized field for adults. The primary feature to identify a baseball field is the infield diamond, but the infield is only a small part of the actual full baseball field. To map a baseball field, the large featureless grassy outfield must be included. The outfields have to be extrapolated out from the infield. In cases where there is a outfield fence, the neural network does quite well at terminating the outfield at the fence. But most baseball fields don't have an outfield fence or even a painted line. The outfields stretch out until they "bump" into something else, a tree line, a road, or another field while maintaining its wedge shape. Complicating the situation, is that like the neural network, the OSM human mappers are also confused about how to map the outfields without a fence! About 10% of the mapped baseball fields are just the infields. 68 | 69 | The phase 1 neural network had no trouble identifying the infields, but it was struggling with baseball outfields without fences. In the 2,800 identified fields, only the baseball fields with excellent outfield were included. Many missing baseball fields had to be skipped because of poor outfield performance. Hopefully the additional high quality outfield data imported into OSM will improve its performance in this challenging area on the next phase. 70 | 71 | ![Problem with Baseball Outfields](/sample-images/phase1-baseball-outfieds.png) 72 | 73 | ## Configuration 74 | 75 | - Ubuntu 17.10 76 | - A Bing key, create a secrets.py file, add in bingKey ="your key" 77 | - Create a virtual environment python 3.6 78 | - In the virtual environment, run "pip install -r requirements.txt" 79 | - TensorFlow 1.3+ 80 | - Keras 2.0.8+. 81 | 82 | -------------------------------------------------------------------------------- /createfinalosm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import imagestoosm.config as osmcfg 4 | import xml.etree.ElementTree as ET 5 | import shapely.geometry as geometry 6 | 7 | def makeOsmFileName( fileNumber): 8 | 9 | return os.path.join( "anomaly","reviewed_{:02d}.osm".format(fileNumber)) 10 | 11 | # clean out the old OSM files. 12 | fileCount = 1 13 | while (os.path.exists(makeOsmFileName(fileCount))): 14 | os.remove( makeOsmFileName(fileCount)) 15 | fileCount += 1 16 | fileCount = 1 17 | 18 | # besides sticking the single accepted OSM files together, this script 19 | # also need to fix up the negative/placeholder ids to be unique in the 20 | # file. 21 | startId = 0 22 | 23 | anomalyStatusFile = os.path.join( "anomaly","status.csv") 24 | 25 | osmTreeRoot = ET.Element('osm') 26 | osmTreeRoot.attrib['version'] = "0.6" 27 | 28 | if ( os.path.exists(anomalyStatusFile)): 29 | with open(anomalyStatusFile,"rt",encoding="ascii") as f: 30 | for line in f: 31 | (status,osmFileName) = line.split(',') 32 | osmFileName = osmFileName.strip() 33 | 34 | if ( status == "accepted") : 35 | tree = ET.parse(osmFileName) 36 | root = tree.getroot() 37 | 38 | wayCount = 0 39 | for node in root.iter('node'): 40 | node.attrib['id'] = "{0:d}".format(int(node.attrib['id'])-startId) 41 | wayCount += 1 42 | osmTreeRoot.append(node) 43 | 44 | for node in root.findall('./way/nd'): 45 | node.attrib['ref'] = "{0:d}".format(int(node.attrib['ref'])-startId) 46 | 47 | for node in root.iter('way'): 48 | node.attrib['id'] = "{0:d}".format(int(node.attrib['id'])-startId) 49 | wayCount += 1 50 | osmTreeRoot.append(node) 51 | 52 | startId += wayCount 53 | 54 | # only only allows 10,000 elements in a single change set upload, make different osm file 55 | # so they can be uploaded without blowing up. 56 | if ( startId > 9500) : 57 | tree = ET.ElementTree(osmTreeRoot) 58 | tree.write( makeOsmFileName(fileCount)) 59 | fileCount += 1 60 | 61 | osmTreeRoot = ET.Element('osm') 62 | osmTreeRoot.attrib['version'] = "0.6" 63 | startId = 0 64 | 65 | tree = ET.ElementTree(osmTreeRoot) 66 | tree.write( makeOsmFileName(fileCount)) 67 | 68 | 69 | -------------------------------------------------------------------------------- /createosmanomaly.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("Mask_RCNN") 3 | 4 | import os 5 | import sys 6 | import glob 7 | import osmmodelconfig 8 | import skimage 9 | import math 10 | import imagestoosm.config as osmcfg 11 | import model as modellib 12 | import visualize as vis 13 | import numpy as np 14 | import csv 15 | import QuadKey.quadkey as quadkey 16 | import shapely.geometry as geometry 17 | import shapely.affinity as affinity 18 | import matplotlib.pyplot as plt 19 | import cv2 20 | import scipy.optimize 21 | import time 22 | from skimage import draw 23 | from skimage import io 24 | 25 | showFigures = False 26 | 27 | def toDegrees(rad): 28 | return rad * 180/math.pi 29 | 30 | def writeOSM( osmFileName,featureName, simpleContour,tilePixel, qkRoot) : 31 | with open(osmFileName,"wt",encoding="ascii") as f: 32 | f.write("\n") 33 | f.write("\n") 34 | id = -1 35 | for pt in simpleContour : 36 | geo = quadkey.TileSystem.pixel_to_geo( (pt[0,0]+tilePixel[0],pt[0,1]+tilePixel[1]),qkRoot.level) 37 | f.write(" \n".format(id,geo[0],geo[1])) 38 | id -= 1 39 | 40 | f.write(" \n".format(id)) 41 | id = -1 42 | for pt in simpleContour : 43 | f.write(" \n".format(id)) 44 | id -= 1 45 | f.write(" \n".format(-1)) 46 | f.write(" \n".format("leisure","pitch")) 47 | f.write(" \n".format("sport",featureName)) 48 | f.write(" \n") 49 | 50 | f.write("\n") 51 | f.close 52 | 53 | def writeShape(wayNumber, finalShape, image, bbTop,bbHeight,bbLeft,bbWidth) : 54 | nPts = int(finalShape.length) 55 | if ( nPts > 5000) : 56 | nPts = 5000 57 | fitContour = np.zeros((nPts,1,2), dtype=np.int32) 58 | 59 | if ( nPts > 3): 60 | 61 | for t in range(0,nPts) : 62 | pt = finalShape.interpolate(t) 63 | fitContour[t,0,0] = pt.x 64 | fitContour[t,0,1] = pt.y 65 | 66 | fitContour = [ fitContour ] 67 | fitContour = [ cv2.approxPolyDP(cnt,2,True) for cnt in fitContour] 68 | 69 | image = np.copy(imageNoMasks) 70 | cv2.drawContours(image, fitContour,-1, (0,255,0), 2) 71 | if ( showFigures ): 72 | fig.add_subplot(2,2,3) 73 | plt.title(featureName + " " + str(r['scores'][i]) + " Fit") 74 | plt.imshow(image[bbTop:bbTop+bbHeight,bbLeft:bbLeft+bbWidth]) 75 | 76 | while ( os.path.exists( "anomaly/add/{0:06d}.osm".format(wayNumber) )) : 77 | wayNumber += 1 78 | 79 | debugFileName = os.path.join( inference_config.ROOT_DIR, "anomaly","add","{0:06d}.jpg".format(wayNumber)) 80 | io.imsave(debugFileName,image[bbTop:bbTop+bbHeight,bbLeft:bbLeft+bbWidth],quality=100) 81 | 82 | osmFileName = os.path.join( inference_config.ROOT_DIR, "anomaly","add","{0:06d}.osm".format(wayNumber)) 83 | writeOSM( osmFileName,featureName, fitContour[0],tilePixel, qkRoot) 84 | 85 | if (showFigures ): 86 | plt.show(block=False) 87 | plt.pause(0.05) 88 | 89 | return wayNumber 90 | 91 | 92 | 93 | ROOT_DIR_ = os.path.dirname(os.path.realpath(sys.argv[0])) 94 | MODEL_DIR = os.path.join(ROOT_DIR_, "logs") 95 | 96 | class InferenceConfig(osmmodelconfig.OsmModelConfig): 97 | GPU_COUNT = 1 98 | IMAGES_PER_GPU = 1 99 | ROOT_DIR = ROOT_DIR_ 100 | 101 | inference_config = InferenceConfig() 102 | 103 | fullTrainingDir = os.path.join( ROOT_DIR_, osmcfg.trainDir,"*") 104 | fullImageList = [] 105 | for imageDir in glob.glob(fullTrainingDir): 106 | if ( os.path.isdir( os.path.join( fullTrainingDir, imageDir) )): 107 | id = os.path.split(imageDir)[1] 108 | fullImageList.append( id) 109 | 110 | # Training dataset 111 | dataset_full = osmmodelconfig.OsmImagesDataset(ROOT_DIR_) 112 | dataset_full.load(fullImageList, inference_config.IMAGE_SHAPE[0], inference_config.IMAGE_SHAPE[1]) 113 | dataset_full.prepare() 114 | 115 | inference_config.display() 116 | 117 | # Recreate the model in inference mode 118 | model = modellib.MaskRCNN(mode="inference", 119 | config=inference_config, 120 | model_dir=MODEL_DIR) 121 | 122 | # Get path to saved weights 123 | # Either set a specific path or find last trained weights 124 | # model_path = os.path.join(ROOT_DIR, ".h5 file name here") 125 | model_path = model.find_last()[1] 126 | print(model_path) 127 | 128 | # Load trained weights (fill in path to trained weights here) 129 | assert model_path != "", "Provide path to trained weights" 130 | print("Loading weights from ", model_path) 131 | model.load_weights(model_path, by_name=True) 132 | 133 | 134 | print("Reading in OSM data") 135 | # load up the OSM features into hash of arrays of polygons, in pixels 136 | features = {} 137 | 138 | for classDir in os.listdir(osmcfg.rootOsmDir) : 139 | classDirFull = os.path.join( osmcfg.rootOsmDir,classDir) 140 | for fileName in os.listdir(classDirFull) : 141 | fullPath = os.path.join( osmcfg.rootOsmDir,classDir,fileName) 142 | with open(fullPath, "rt") as csvfile: 143 | csveader = csv.reader(csvfile, delimiter='\t') 144 | 145 | pts = [] 146 | for row in csveader: 147 | latLot = (float(row[0]),float(row[1])) 148 | pixel = quadkey.TileSystem.geo_to_pixel(latLot,osmcfg.tileZoom) 149 | 150 | pts.append(pixel) 151 | 152 | feature = { 153 | "geometry" : geometry.Polygon(pts), 154 | "filename" : fullPath 155 | } 156 | 157 | 158 | if ( (classDir in features) == False) : 159 | features[classDir] = [] 160 | 161 | features[classDir].append( feature ) 162 | 163 | 164 | # make the output dirs, a fresh start is possible just by deleting anomaly 165 | if ( not os.path.isdir("anomaly")) : 166 | os.mkdir("anomaly") 167 | if ( not os.path.isdir("anomaly/add")) : 168 | os.mkdir("anomaly/add") 169 | if ( not os.path.isdir("anomaly/replace")) : 170 | os.mkdir("anomaly/replace") 171 | if ( not os.path.isdir("anomaly/overlap")) : 172 | os.mkdir("anomaly/overlap") 173 | 174 | fig = {} 175 | if ( showFigures): 176 | fig = plt.figure() 177 | 178 | wayNumber = 0 179 | 180 | startTime = time.time() 181 | 182 | count = 1 183 | for image_index in dataset_full.image_ids : 184 | currentTime = time.time() 185 | howLong = currentTime-startTime 186 | secPerImage = howLong/count 187 | imagesLeft = len(dataset_full.image_ids)-count 188 | timeLeftHrs = (imagesLeft*secPerImage)/3600.0 189 | 190 | print("Processing {} of {} {:2.1f} hrs left".format(count,len(dataset_full.image_ids),timeLeftHrs)) 191 | count += 1 192 | 193 | image, image_meta, gt_class_id, gt_bbox, gt_mask = modellib.load_image_gt(dataset_full, inference_config,image_index, use_mini_mask=False) 194 | info = dataset_full.image_info[image_index] 195 | 196 | # get the pixel location for this training image. 197 | metaFileName = os.path.join( inference_config.ROOT_DIR, osmcfg.trainDir,info['id'],info['id']+".txt") 198 | 199 | quadKeyStr = "" 200 | with open(metaFileName) as metafile: 201 | quadKeyStr = metafile.readline() 202 | 203 | quadKeyStr = quadKeyStr.strip() 204 | qkRoot = quadkey.from_str(quadKeyStr) 205 | tilePixel = quadkey.TileSystem.geo_to_pixel(qkRoot.to_geo(), qkRoot.level) 206 | 207 | # run the network 208 | results = model.detect([image], verbose=0) 209 | r = results[0] 210 | 211 | maxImageSize = 256*3 212 | featureMask = np.zeros((maxImageSize, maxImageSize), dtype=np.uint8) 213 | 214 | pts = [] 215 | pts.append( ( tilePixel[0]+0,tilePixel[1]+0 ) ) 216 | pts.append( ( tilePixel[0]+0,tilePixel[1]+maxImageSize ) ) 217 | pts.append( ( tilePixel[0]+maxImageSize,tilePixel[1]+maxImageSize ) ) 218 | pts.append( ( tilePixel[0]+maxImageSize,tilePixel[1]+0 ) ) 219 | 220 | imageBoundingBoxPoly = geometry.Polygon(pts) 221 | 222 | foundFeatures = {} 223 | 224 | for featureType in osmmodelconfig.featureNames.keys() : 225 | foundFeatures[featureType ] = [] 226 | 227 | for feature in features[featureType] : 228 | if ( imageBoundingBoxPoly.intersects( feature['geometry']) ) : 229 | 230 | xs, ys = feature['geometry'].exterior.coords.xy 231 | 232 | outOfRangeCount = len([ x for x in xs if x < tilePixel[0] or x >= tilePixel[0]+maxImageSize ]) 233 | outOfRangeCount += len([ y for y in ys if y < tilePixel[1] or y >= tilePixel[1]+maxImageSize ]) 234 | 235 | if ( outOfRangeCount == 0) : 236 | foundFeatures[featureType ].append( feature) 237 | 238 | # draw black lines showing where osm data is 239 | for featureType in osmmodelconfig.featureNames.keys() : 240 | for feature in foundFeatures[featureType] : 241 | xs, ys = feature['geometry'].exterior.coords.xy 242 | 243 | xs = [ x-tilePixel[0] for x in xs] 244 | ys = [ y-tilePixel[1] for y in ys] 245 | 246 | rr, cc = draw.polygon_perimeter(xs,ys,(maxImageSize,maxImageSize)) 247 | image[cc,rr] = 0 248 | 249 | imageNoMasks = np.copy(image) 250 | 251 | for i in range( len(r['class_ids'])) : 252 | mask = r['masks'][:,:,i] 253 | edgePixels = 15 254 | outside = np.sum( mask[0:edgePixels,:]) + np.sum( mask[-edgePixels:-1,:]) + np.sum( mask[:,0:edgePixels]) + np.sum( mask[:,-edgePixels:-1]) 255 | 256 | image = np.copy(imageNoMasks) 257 | 258 | if ( r['scores'][i] > 0.98 and outside == 0 ) : 259 | featureFound = False 260 | for featureType in osmmodelconfig.featureNames.keys() : 261 | for feature in foundFeatures[featureType] : 262 | classId = osmmodelconfig.featureNames[featureType] 263 | 264 | if ( classId == r['class_ids'][i] ) : 265 | 266 | xs, ys = feature['geometry'].exterior.coords.xy 267 | 268 | xs = [ x-tilePixel[0] for x in xs] 269 | ys = [ y-tilePixel[1] for y in ys] 270 | 271 | xsClipped = [ min( max( x,0),maxImageSize) for x in xs] 272 | ysClipped = [ min( max( y,0),maxImageSize) for y in ys] 273 | 274 | featureMask.fill(0) 275 | rr, cc = draw.polygon(xs,ys,(maxImageSize,maxImageSize)) 276 | featureMask[cc,rr] = 1 277 | 278 | maskAnd = featureMask * mask 279 | overlap = np.sum(maskAnd ) 280 | 281 | if ( outside == 0 and overlap > 0) : 282 | featureFound = True 283 | 284 | if ( featureFound == False) : 285 | weight = 0.25 286 | 287 | # get feature name 288 | featureName = "" 289 | for featureType in osmmodelconfig.featureNames.keys() : 290 | if ( osmmodelconfig.featureNames[featureType] == r['class_ids'][i] ) : 291 | featureName = featureType 292 | 293 | #if ( r['class_ids'][i] == 1): 294 | # vis.apply_mask(image,mask,[weight,0,0]) 295 | #if ( r['class_ids'][i] == 2): 296 | # vis.apply_mask(image,mask,[weight,weight,0]) 297 | #if ( r['class_ids'][i] == 3): 298 | # vis.apply_mask(image,mask,[0.0,0,weight]) 299 | 300 | mask = mask.astype(np.uint8) 301 | mask = mask * 255 302 | 303 | ret,thresh = cv2.threshold(mask,127,255,0) 304 | im2, rawContours,h = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE) 305 | 306 | bbLeft,bbTop,bbWidth,bbHeight = cv2.boundingRect(rawContours[0]) 307 | 308 | bbBuffer = 75 309 | 310 | bbLeft = max(bbLeft-bbBuffer,0) 311 | bbRight = min(bbLeft+2*bbBuffer+bbWidth,maxImageSize) 312 | bbWidth = bbRight-bbLeft 313 | 314 | bbTop = max(bbTop-bbBuffer,0) 315 | bbBottom = min(bbTop+2*bbBuffer+bbHeight,maxImageSize-1) 316 | bbHeight = bbBottom-bbTop 317 | 318 | image = np.copy(imageNoMasks) 319 | cv2.drawContours(image, rawContours,-1, (0,255,0), 2) 320 | 321 | if ( showFigures ): 322 | fig.add_subplot(2,2,1) 323 | plt.title(featureName + " " + str(r['scores'][i]) + " Raw") 324 | plt.imshow(image[bbTop:bbTop+bbHeight,bbLeft:bbLeft+bbWidth]) 325 | 326 | simpleContour = [ cv2.approxPolyDP(cnt,5,True) for cnt in rawContours] 327 | image = np.copy(imageNoMasks) 328 | cv2.drawContours(image, simpleContour,-1, (0,255,0), 2) 329 | if ( showFigures ): 330 | fig.add_subplot(2,2,2) 331 | plt.title(featureName + " " + str(r['scores'][i]) + " Simplify") 332 | plt.imshow(image[bbTop:bbTop+bbHeight,bbLeft:bbLeft+bbWidth]) 333 | 334 | simpleContour = simpleContour[0] 335 | 336 | print(" {}".format(featureName)) 337 | if ( featureName == "baseball" and isinstance(simpleContour,np.ndarray) ): 338 | 339 | while ( os.path.exists( "anomaly/add/{0:06d}.osm".format(wayNumber) )) : 340 | wayNumber += 1 341 | 342 | debugFileName = os.path.join( inference_config.ROOT_DIR, "anomaly","add","{0:06d}.jpg".format(wayNumber)) 343 | io.imsave(debugFileName,image[bbTop:bbTop+bbHeight,bbLeft:bbLeft+bbWidth],quality=100) 344 | 345 | osmFileName = os.path.join( inference_config.ROOT_DIR, "anomaly","add","{0:06d}.osm".format(wayNumber)) 346 | writeOSM( osmFileName,featureName, simpleContour,tilePixel, qkRoot) 347 | 348 | fitContour = simpleContour 349 | 350 | if ( featureName == 'baseball' ) : 351 | 352 | def makePie(paramsX): 353 | centerX,centerY,width,angle = paramsX 354 | 355 | pts = [] 356 | 357 | pts.append((0,0)) 358 | pts.append((width,0)) 359 | 360 | step = math.pi/10 361 | r = step 362 | while r < math.pi/2: 363 | x = math.cos(r)*width 364 | y = math.sin(r)*width 365 | pts.append( (x,y) ) 366 | r += step 367 | 368 | pts.append( (0,width)) 369 | pts.append( (0,0)) 370 | 371 | fitShape = geometry.LineString(pts) 372 | 373 | fitShape = affinity.translate(fitShape, -width/2,-width/2 ) 374 | fitShape = affinity.rotate(fitShape,angle ) 375 | fitShape = affinity.translate(fitShape, centerX,centerY ) 376 | 377 | return fitShape 378 | 379 | def fitPie(paramsX): 380 | fitShape = makePie(paramsX) 381 | 382 | huberCutoff = 5 383 | 384 | sum = 0 385 | for cnt in rawContours: 386 | for pt in cnt: 387 | p = geometry.Point(pt[0]) 388 | d = p.distance(fitShape) 389 | 390 | if ( d < huberCutoff) : 391 | sum += 0.5 * d * d 392 | else: 393 | sum += huberCutoff*(math.fabs(d)-0.5*huberCutoff) 394 | 395 | return sum 396 | 397 | cm = np.mean( rawContours[0],axis=0) 398 | 399 | results = [] 400 | angleStepCount = 8 401 | for angleI in range(angleStepCount): 402 | 403 | centerX = cm[0,0] 404 | centerY = cm[0,1] 405 | width = math.sqrt(cv2.contourArea(rawContours[0])) 406 | angle = 360 * float(angleI)/angleStepCount 407 | x0 = np.array([centerX,centerY,width,angle ]) 408 | 409 | resultR = scipy.optimize.minimize(fitPie, x0, method='nelder-mead', options={'xtol': 1e-6,'maxiter':50 }) 410 | 411 | results.append(resultR) 412 | 413 | bestScore = 1e100 414 | bestResult = {} 415 | for result in results: 416 | if result.fun < bestScore : 417 | bestScore = result.fun 418 | bestResult = result 419 | 420 | bestResult = scipy.optimize.minimize(fitPie, bestResult.x, method='nelder-mead', options={'xtol': 1e-6 }) 421 | finalShape = makePie(bestResult.x) 422 | wayNumber = writeShape(wayNumber, finalShape, image, bbTop,bbHeight,bbLeft,bbWidth) 423 | 424 | for result in results: 425 | angle = result.x[3] 426 | angleDelta = int(math.fabs(result.x[3]-bestResult.x[3])) % 360 427 | if result.fun < 1.2*bestScore and angleDelta > 45 : 428 | result = scipy.optimize.minimize(fitPie, result.x, method='nelder-mead', options={'xtol': 1e-6 }) 429 | finalShape = makePie(result.x) 430 | wayNumber = writeShape(wayNumber, finalShape, image, bbTop,bbHeight,bbLeft,bbWidth) 431 | 432 | else: 433 | 434 | def makeRect(paramsX): 435 | centerX,centerY,width,height,angle = paramsX 436 | 437 | pts = [ 438 | (-width/2,height/2), 439 | (width/2,height/2), 440 | (width/2,-height/2), 441 | (-width/2,-height/2), 442 | (-width/2,height/2)] 443 | 444 | fitShape = geometry.LineString(pts) 445 | 446 | fitShape = affinity.rotate(fitShape, angle,use_radians=True ) 447 | fitShape = affinity.translate(fitShape, centerX,centerY ) 448 | 449 | return fitShape 450 | 451 | def fitRect(paramsX): 452 | fitShape = makeRect(paramsX) 453 | 454 | sum = 0 455 | 456 | for cnt in rawContours: 457 | for pt in cnt: 458 | p = geometry.Point(pt[0]) 459 | d = p.distance(fitShape) 460 | sum += d*d 461 | return sum 462 | 463 | cm = np.mean( rawContours[0],axis=0) 464 | 465 | result = {} 466 | angleStepCount = 8 467 | for angleI in range(angleStepCount): 468 | 469 | centerX = cm[0,0] 470 | centerY = cm[0,1] 471 | width = math.sqrt(cv2.contourArea(rawContours[0])) 472 | height = width 473 | angle = 2*math.pi * float(angleI)/angleStepCount 474 | x0 = np.array([centerX,centerY,width,height,angle ]) 475 | resultR = scipy.optimize.minimize(fitRect, x0, method='nelder-mead', options={'xtol': 1e-6,'maxiter':50 }) 476 | 477 | if ( angleI == 0): 478 | result = resultR 479 | 480 | if ( resultR.fun < result.fun): 481 | result = resultR 482 | #print("{} {}".format(angle * 180.0 / math.pi,resultR.fun )) 483 | 484 | resultR = scipy.optimize.minimize(fitRect, resultR.x, method='nelder-mead', options={'xtol': 1e-6 }) 485 | 486 | #print(result) 487 | finalShape = makeRect(result.x) 488 | 489 | wayNumber = writeShape(wayNumber, finalShape, image, bbTop,bbHeight,bbLeft,bbWidth) 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | -------------------------------------------------------------------------------- /findsmallbaseball.py: -------------------------------------------------------------------------------- 1 | import imagestoosm.config as cfg 2 | import os 3 | import QuadKey.quadkey as quadkey 4 | import numpy as np 5 | import shapely.geometry as geometry 6 | from skimage import draw 7 | from skimage import io 8 | import csv 9 | 10 | 11 | # load up the OSM features into hash of arrays of polygons, in pixels 12 | 13 | for classDir in os.listdir(cfg.rootOsmDir) : 14 | if ( classDir == 'baseball') : 15 | classDirFull = os.path.join( cfg.rootOsmDir,classDir) 16 | for fileName in os.listdir(classDirFull) : 17 | fullPath = os.path.join( cfg.rootOsmDir,classDir,fileName) 18 | with open(fullPath, "rt") as csvfile: 19 | csveader = csv.reader(csvfile, delimiter='\t') 20 | 21 | pts = [] 22 | for row in csveader: 23 | latLot = (float(row[0]),float(row[1])) 24 | pixel = quadkey.TileSystem.geo_to_pixel(latLot,cfg.tileZoom) 25 | 26 | pts.append(pixel) 27 | 28 | poly = geometry.Polygon(pts); 29 | 30 | areaMeters = poly.area * 0.596 *0.596; 31 | 32 | print("{}\t{}".format(fileName,areaMeters)) 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /getdatafromosm.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import overpy 3 | import imagestoosm.config as cfg 4 | import os 5 | import shapely.geometry 6 | import shapely.wkt 7 | import shapely.ops 8 | import geojson 9 | 10 | api = overpy.Overpass() 11 | 12 | summary = {} 13 | 14 | def saveOsmData(query) : 15 | 16 | result = api.query(query) 17 | 18 | for way in result.ways: 19 | 20 | # "leisure=pitch,sport=" , don't use "-" char" in featureDirectoryName 21 | featureDirectoryName = way.tags.get("sport") 22 | 23 | outputDirectoryName = os.path.join(cfg.rootOsmDir,featureDirectoryName) 24 | if ( os.path.exists(outputDirectoryName) == False): 25 | os.makedirs(outputDirectoryName) 26 | 27 | if ( (featureDirectoryName in summary) == False) : 28 | summary[featureDirectoryName] = 1 29 | else: 30 | summary[featureDirectoryName] += 1 31 | 32 | filenameBase= os.path.join(cfg.rootOsmDir,featureDirectoryName,str(way.id)) 33 | 34 | #print("Name: %d %s %s" % ( way.id ,way.tags.get("name", ""),filenameBase)) 35 | 36 | # leave the csv file for now, will delete when the script for the next 37 | # stage is rewritten. 38 | with open("%s.csv" % (filenameBase), "wt") as text_file: 39 | for node in way.nodes: 40 | text_file.write("%0.7f\t%0.7f\n" % (node.lat, node.lon)) 41 | 42 | with open("%s.GeoJSON" % (filenameBase), "wt") as text_file: 43 | 44 | rawNodes = [] 45 | for node in way.nodes: 46 | rawNodes.append( (node.lon, node.lat) ) 47 | 48 | try: 49 | geom = shapely.geometry.Polygon(rawNodes) 50 | 51 | tags = way.tags 52 | tags['wayOSMId'] = way.id 53 | 54 | features =[] 55 | features.append( geojson.Feature(geometry=geom, properties=tags)) 56 | 57 | featureC = geojson.FeatureCollection(features) 58 | 59 | text_file.write(geojson.dumps(featureC)) 60 | except Exception as e: 61 | print(e) 62 | 63 | 64 | queryFull = """[timeout:125]; 65 | ( 66 | area[admin_level=4][boundary=administrative][name="Massachusetts"]; 67 | area[admin_level=4][boundary=administrative][name="New York"]; 68 | area[admin_level=4][boundary=administrative][name="Connecticut"]; 69 | area[admin_level=4][boundary=administrative][name="Rhode Island"]; 70 | area[admin_level=4][boundary=administrative][name="Pennsylvania"]; 71 | )->.searchArea; 72 | ( 73 | way["sport"="baseball"]["leisure"="pitch"](area.searchArea); 74 | way["sport"="tennis"]["leisure"="pitch"](area.searchArea); 75 | way["sport"="soccer"]["leisure"="pitch"](area.searchArea); 76 | way["sport"="american_football"]["leisure"="pitch"](area.searchArea); 77 | way["sport"="basketball"]["leisure"="pitch"](area.searchArea); 78 | ); 79 | (._;>;); 80 | out body; 81 | """ 82 | 83 | # fetch all ways and nodes 84 | queryMA = """[timeout:125]; 85 | ( 86 | area[admin_level=4][boundary=administrative][name="Massachusetts"]; 87 | )->.searchArea; 88 | ( 89 | way["sport"="baseball"]["leisure"="pitch"](area.searchArea); 90 | way["sport"="tennis"]["leisure"="pitch"](area.searchArea); 91 | way["sport"="soccer"]["leisure"="pitch"](area.searchArea); 92 | way["sport"="american_football"]["leisure"="pitch"](area.searchArea); 93 | way["sport"="basketball"]["leisure"="pitch"](area.searchArea); 94 | ); 95 | (._;>;); 96 | out body; 97 | """ 98 | saveOsmData(queryFull) 99 | 100 | # Other possible data to query 101 | # - bridges 102 | # - solar panels farms 103 | # - wind turbines 104 | # - railroad crossings. 105 | # - active rail roads 106 | # - water tanks 107 | # - wafer/lakes/rivers 108 | # - parking lots 109 | # - driveways 110 | # - gas stations 111 | # - building (Microsoft has already done this) 112 | # - Running track 113 | 114 | print(summary) 115 | -------------------------------------------------------------------------------- /gettilesfrombing.py: -------------------------------------------------------------------------------- 1 | # for each feature in osm/baseball directory, get each point and make sure we have the 2 | # tile for that point in the tile cache. 3 | 4 | import requests 5 | import os 6 | import os.path 7 | import csv 8 | import QuadKey.quadkey as quadkey 9 | import shutil 10 | import imagestoosm.config as cfg 11 | import imagestoosm.secrets as secrets 12 | from random import random 13 | from time import sleep 14 | 15 | # MS doesn't want you hardcoding the URLs to the tile server. This request asks for the Aerial 16 | # url template. Replace {quadkey}, and {subdomain} 17 | response = requests.get("https://dev.virtualearth.net/REST/V1/Imagery/Metadata/Aerial?key=%s" % (secrets.bingKey)) 18 | 19 | data = response.json() 20 | 21 | # grabs the data we need from the response. 22 | tileUrlTemplate = data['resourceSets'][0]['resources'][0]['imageUrl'] 23 | imageDomains = data['resourceSets'][0]['resources'][0]['imageUrlSubdomains'] 24 | 25 | if ( os.path.exists(cfg.rootTileDir) == False) : 26 | os.mkdir(cfg.rootTileDir) 27 | 28 | bingTilesDir = os.path.join( cfg.rootTileDir,"bing_z" + str( cfg.tileZoom)) 29 | 30 | if ( os.path.exists(bingTilesDir) == False) : 31 | os.mkdir(bingTilesDir) 32 | 33 | for classDir in os.listdir(cfg.rootOsmDir) : 34 | classDirFull = os.path.join( cfg.rootOsmDir,classDir) 35 | for fileName in os.listdir(classDirFull) : 36 | fullPath = os.path.join( cfg.rootOsmDir,classDir,fileName) 37 | with open(fullPath, "rt") as csvfile: 38 | csveader = csv.reader(csvfile, delimiter='\t') 39 | print("%s " % (fullPath),end='') 40 | 41 | neededTile = False 42 | for row in csveader: 43 | 44 | tilePixel = quadkey.TileSystem.geo_to_pixel((float(row[0]),float(row[1])), cfg.tileZoom) 45 | 46 | for x in range(-2,3) : 47 | for y in range(-2,3) : 48 | pixel = ( tilePixel[0] + 256*x, tilePixel[1]+256*y) 49 | geo = quadkey.TileSystem.pixel_to_geo(pixel, cfg.tileZoom) 50 | qk = quadkey.from_geo(geo,cfg.tileZoom) 51 | 52 | qkStr = str(qk) 53 | 54 | tileCacheDir = os.path.join(bingTilesDir,qkStr[-3:]) 55 | 56 | if ( os.path.exists(tileCacheDir) == False) : 57 | os.mkdir( tileCacheDir) 58 | 59 | tileFileName = "%s/%s.jpg" % (tileCacheDir, qkStr) 60 | 61 | if ( os.path.exists(tileFileName) ) : 62 | # already downloaded 63 | ok = 1; 64 | else : 65 | print("T",end='') 66 | url = tileUrlTemplate.replace("{subdomain}",imageDomains[0]) 67 | url = url.replace("{quadkey}",qkStr) 68 | url = "%s&key=%s" % (url,secrets.bingKey) 69 | 70 | response = requests.get(url,stream=True) 71 | 72 | with open(tileFileName,'wb') as out_file: 73 | shutil.copyfileobj(response.raw, out_file) 74 | 75 | del response 76 | neededTile = True 77 | 78 | print("") 79 | 80 | if ( neededTile ): 81 | sleep(random()*3) 82 | 83 | 84 | -------------------------------------------------------------------------------- /imagestoosm/__init__.py: -------------------------------------------------------------------------------- 1 | #import config 2 | #import secrets -------------------------------------------------------------------------------- /imagestoosm/config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | tileZoom = 18 4 | rootOsmDir = "osm" 5 | rootTileDir = "tiles" 6 | trainDir = "train-images" 7 | trainImageSize = 512 -------------------------------------------------------------------------------- /maketrainingimages.py: -------------------------------------------------------------------------------- 1 | import imagestoosm.config as cfg 2 | import os 3 | import QuadKey.quadkey as quadkey 4 | import numpy as np 5 | import shapely.geometry as geometry 6 | from skimage import draw 7 | from skimage import io 8 | import csv 9 | 10 | minFeatureClip = 0.3 11 | 12 | # make the training data images 13 | 14 | # construct index of osm data, each point 15 | # for each tile 16 | # check for tiles east and south, and south/east to make 512x512 tile, if no skip 17 | # bounding box for image 18 | # write out image as png 19 | # see what features overlap with image 20 | 21 | # if more than >20% 22 | # lit of augmentations (N +45,-45 degree), N offsets 23 | # emit mask for feature for current image. 24 | 25 | # training will do flips, intensity, and color shifts if needed 26 | 27 | 28 | os.system("rm -R " + cfg.trainDir) 29 | os.mkdir(cfg.trainDir) 30 | 31 | # load up the OSM features into hash of arrays of polygons, in pixels 32 | features = {} 33 | 34 | for classDir in os.listdir(cfg.rootOsmDir) : 35 | classDirFull = os.path.join( cfg.rootOsmDir,classDir) 36 | for fileName in os.listdir(classDirFull) : 37 | fullPath = os.path.join( cfg.rootOsmDir,classDir,fileName) 38 | with open(fullPath, "rt") as csvfile: 39 | csveader = csv.reader(csvfile, delimiter='\t') 40 | 41 | pts = [] 42 | for row in csveader: 43 | latLot = (float(row[0]),float(row[1])) 44 | pixel = quadkey.TileSystem.geo_to_pixel(latLot,cfg.tileZoom) 45 | 46 | pts.append(pixel) 47 | 48 | poly = geometry.Polygon(pts) 49 | 50 | areaMeters = poly.area * 0.596 *0.596 51 | 52 | # don't learn against baseball fields that are outlines just on the 53 | # diamond. They are tagged wrong, don't want to teach the NN that this 54 | # is correct. There are > 1000 of them in the OSM DB, we can't avoid 55 | # them. 56 | if ( classDir != "baseball" or areaMeters > 2500) : 57 | feature = { 58 | "geometry" : poly, 59 | "filename" : fullPath 60 | } 61 | 62 | if ( (classDir in features) == False) : 63 | features[classDir] = [] 64 | 65 | features[classDir].append( feature ) 66 | 67 | imageWriteCounter = 0 68 | for root, subFolders, files in os.walk(cfg.rootTileDir): 69 | 70 | for file in files: 71 | 72 | quadKeyStr = os.path.splitext(file)[0] 73 | 74 | qkRoot = quadkey.from_str(quadKeyStr) 75 | tilePixel = quadkey.TileSystem.geo_to_pixel(qkRoot.to_geo(), qkRoot.level) 76 | 77 | tileRootDir = os.path.split( root)[0] 78 | 79 | # stick the adjacent tiles together to make larger images up to max 80 | # image size. 81 | maxImageSize = 256*3 82 | maxTileCount = maxImageSize // 256 83 | count = 0 84 | image = np.zeros([maxImageSize,maxImageSize,3],dtype=np.uint8) 85 | for x in range(maxTileCount) : 86 | for y in range(maxTileCount) : 87 | pixel = ( tilePixel[0] + 256*x, tilePixel[1]+256*y) 88 | geo = quadkey.TileSystem.pixel_to_geo(pixel, qkRoot.level) 89 | qk = quadkey.from_geo(geo, qkRoot.level) 90 | 91 | qkStr = str(qk) 92 | 93 | tileCacheDir = os.path.join(tileRootDir,qkStr[-3:]) 94 | 95 | tileFileName = "%s/%s.jpg" % (tileCacheDir, qkStr) 96 | 97 | if ( os.path.exists(tileFileName) ) : 98 | try: 99 | image[ y*256 : (y+1)*256, x*256 : (x+1)*256,0:3 ] = io.imread(tileFileName ) 100 | count += 1 101 | except: 102 | # try to get the tile again next time. 103 | os.remove( tileFileName) 104 | 105 | 106 | pts = [] 107 | pts.append( ( tilePixel[0]+0,tilePixel[1]+0 ) ) 108 | pts.append( ( tilePixel[0]+0,tilePixel[1]+maxImageSize ) ) 109 | pts.append( ( tilePixel[0]+maxImageSize,tilePixel[1]+maxImageSize ) ) 110 | pts.append( ( tilePixel[0]+maxImageSize,tilePixel[1]+0 ) ) 111 | 112 | imageBoundingBoxPoly = geometry.Polygon(pts) 113 | 114 | featureMask = np.zeros((maxImageSize, maxImageSize), dtype=np.uint8) 115 | featureCountTotal = 0 116 | usedFileNames = [] 117 | for featureType in features : 118 | featureCount = 0 119 | for feature in features[featureType] : 120 | if ( imageBoundingBoxPoly.intersects( feature['geometry']) ) : 121 | area = feature['geometry'].area 122 | 123 | xs, ys = feature['geometry'].exterior.coords.xy 124 | xs = [ x-tilePixel[0] for x in xs] 125 | ys = [ y-tilePixel[1] for y in ys] 126 | 127 | xsClipped = [ min( max( x,0),maxImageSize) for x in xs] 128 | ysClipped = [ min( max( y,0),maxImageSize) for y in ys] 129 | 130 | pts2 = [] 131 | for i in range(len(xs)) : 132 | pts2.append( (xsClipped[i],ysClipped[i] ) ) 133 | 134 | clippedPoly = geometry.Polygon(pts2) 135 | newArea = clippedPoly.area 136 | 137 | if ( area > 0 and newArea/area > minFeatureClip) : 138 | 139 | if (os.path.exists( "%s/%06d" % (cfg.trainDir,imageWriteCounter) ) == False) : 140 | os.mkdir( "%s/%06d" % (cfg.trainDir,imageWriteCounter) ) 141 | 142 | featureMask.fill(0) 143 | rr, cc = draw.polygon(xs,ys,(maxImageSize,maxImageSize)) 144 | featureMask[cc,rr] = 255 145 | io.imsave("%s/%06d/%06d-%s-%d.png" % (cfg.trainDir,imageWriteCounter,imageWriteCounter,featureType,featureCount),featureMask) 146 | usedFileNames.append( feature['filename'] ) 147 | featureCount += 1 148 | featureCountTotal += 1 149 | 150 | if ( featureCountTotal > 0) : 151 | io.imsave("%s/%06d/%06d.jpg" % (cfg.trainDir,imageWriteCounter,imageWriteCounter),image,quality=100) 152 | 153 | with open("%s/%06d/%06d.txt" % (cfg.trainDir,imageWriteCounter,imageWriteCounter), "wt") as text_file: 154 | text_file.write( "%s\n" % (str(qkRoot))) 155 | text_file.write( "%0.8f,%0.8f\n" % qkRoot.to_geo()) 156 | for f in usedFileNames : 157 | text_file.write( "%s\n" % (f)) 158 | 159 | imageWriteCounter += 1 160 | 161 | print("%s - %s - tiles %d - features %d" % (os.path.join(root, file), quadKeyStr,count, featureCountTotal)) 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /osmmodelconfig.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("Mask_RCNN") 3 | 4 | import os 5 | import random 6 | import math 7 | import re 8 | import time 9 | import numpy as np 10 | import cv2 11 | import matplotlib 12 | import random 13 | import glob 14 | import skimage 15 | 16 | from config import Config 17 | import imagestoosm.config as osmcfg 18 | import utils 19 | import model as modellib 20 | import visualize 21 | from model import log 22 | 23 | featureNames = { 24 | "baseball":1, 25 | "basketball":2, 26 | "tennis":3 27 | # "american_football":4, 28 | # "soccer":5, 29 | } 30 | 31 | class OsmModelConfig(Config): 32 | """Configuration for training on the toy shapes dataset. 33 | Derives from the base Config class and overrides values specific 34 | to the toy shapes dataset. 35 | """ 36 | # Give the configuration a recognizable name 37 | NAME = "OSM Images Baseball,Basketball,Tennis" 38 | 39 | # Batch size is (GPUs * images/GPU). 40 | GPU_COUNT = 1 41 | IMAGES_PER_GPU = 2 42 | LEARNING_RATE = 0.001 43 | 44 | # 2 minutes 45 | #STEPS_PER_EPOCH = 100 // IMAGES_PER_GPU 46 | 47 | # 1 hour epoch 48 | STEPS_PER_EPOCH = 12000 // IMAGES_PER_GPU 49 | 50 | # Number of classes (including background) 51 | NUM_CLASSES = 1 + len(featureNames) # background + featureType's 52 | 53 | # Each tile is 256 pixels across, training data is 3x3 tiles 54 | TILES=3 55 | IMAGE_MIN_DIM = 256*TILES 56 | IMAGE_MAX_DIM = 256*TILES 57 | 58 | MINI_MASK_SHAPE = (128, 128) 59 | #MASK_SHAPE = (IMAGE_MIN_DIM, IMAGE_MIN_DIM) 60 | 61 | # Reduce training ROIs per image because the images are small and have 62 | # few objects. Aim to allow ROI sampling to pick 33% positive ROIs. 63 | #TRAIN_ROIS_PER_IMAGE = 64 64 | #DETECTION_MAX_INSTANCES = 64 65 | 66 | VALIDATION_STEPS = 100 67 | 68 | class OsmImagesDataset(utils.Dataset): 69 | 70 | def __init__(self, rootDir): 71 | utils.Dataset.__init__(self) 72 | self.ROOT_DIR = rootDir 73 | 74 | def load(self, imageDirs, height, width): 75 | """Generate the requested number of synthetic images. 76 | count: number of images to generate. 77 | height, width: the size of the generated images. 78 | """ 79 | 80 | for feature in featureNames: 81 | self.add_class("osm", featureNames[feature],feature) 82 | 83 | # Add images 84 | for i in range(len(imageDirs)): 85 | imgPath = os.path.join( self.ROOT_DIR, osmcfg.trainDir,imageDirs[i],imageDirs[i] + ".jpg") 86 | self.add_image("osm", image_id=imageDirs[i], path=imgPath, width=width, height=height) 87 | 88 | def load_mask(self, image_id): 89 | """Generate instance masks for shapes of the given image ID. 90 | """ 91 | info = self.image_info[image_id] 92 | 93 | imgDir = os.path.join( self.ROOT_DIR, osmcfg.trainDir,info['id']) 94 | wildcard = os.path.join( imgDir,"*.png") 95 | 96 | # 00015-american_football-0.png 00015-baseball-0.png 00015-baseball-1.png 00015-baseball-2.png 00015-baseball-3.png 00015-basketball-0.png 00015-basketball-1.png 00015.jpg 00015.txt 97 | 98 | maskCount = 0 99 | for filePath in glob.glob(wildcard): 100 | filename = os.path.split(filePath)[1] 101 | parts = filename.split( "-") 102 | if ( len(parts) == 3) and parts[1] in featureNames: 103 | maskCount += 1 104 | 105 | mask = np.zeros([info['height'], info['width'], maskCount], dtype=np.uint8) 106 | class_ids = np.zeros((maskCount), np.int32) 107 | 108 | count = 0 109 | for filePath in glob.glob(wildcard): 110 | filename = os.path.split(filePath)[1] 111 | parts = filename.split( "-") 112 | if ( len(parts) == 3) and parts[1] in featureNames: 113 | imgPath = filePath 114 | mask[:, :, count] = skimage.io.imread(filePath) 115 | class_ids[count] = featureNames[parts[1]] 116 | count += 1 117 | 118 | return mask, class_ids 119 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | apturl==0.5.2 2 | asn1crypto==0.22.0 3 | bleach==1.5.0 4 | Brlapi==0.6.5 5 | certifi==2017.4.17 6 | chardet==3.0.4 7 | click==6.7 8 | click-plugins==1.0.3 9 | cligj==0.4.0 10 | command-not-found==0.3 11 | cryptography==1.9 12 | cupshelpers==1.0 13 | cycler==0.10.0 14 | Cython==0.25.2 15 | decorator==4.1.2 16 | defer==1.0.6 17 | descartes==1.1.0 18 | distro-info==0.17 19 | entrypoints==0.2.3 20 | Fiona==1.7.10.post1 21 | geojson==2.3.0 22 | geopandas==0.3.0 23 | h5py==2.7.1 24 | html5lib==0.9999999 25 | httplib2==0.9.2 26 | idna==2.5 27 | ipykernel==4.6.1 28 | ipython==6.2.1 29 | ipython-genutils==0.2.0 30 | ipywidgets==7.0.4 31 | jedi==0.11.0 32 | Jinja2==2.10 33 | jsonschema==2.6.0 34 | jupyter==1.0.0 35 | jupyter-client==5.1.0 36 | jupyter-console==5.2.0 37 | jupyter-core==4.4.0 38 | Keras==2.0.9 39 | keyring==10.4.0 40 | keyrings.alt==2.2 41 | language-selector==0.1 42 | launchpadlib==1.10.5 43 | lazr.restfulclient==0.13.5 44 | lazr.uri==1.0.3 45 | louis==3.0.0 46 | Mako==1.0.7 47 | Markdown==2.6.9 48 | MarkupSafe==1.0 49 | matplotlib==2.0.0 50 | mistune==0.8.1 51 | munch==2.2.0 52 | nbconvert==5.3.1 53 | nbformat==4.4.0 54 | networkx==1.11 55 | nose==1.3.7 56 | notebook==5.2.1 57 | numpy==1.13.3 58 | oauth==1.0.1 59 | olefile==0.44 60 | overpy==0.4 61 | pandas==0.21.0 62 | pandocfilters==1.4.2 63 | parso==0.1.0 64 | pexpect==4.2.1 65 | pickleshare==0.7.4 66 | Pillow==4.1.1 67 | pkg-resources==0.0.0 68 | prompt-toolkit==1.0.15 69 | protobuf==3.4.0 70 | ptyprocess==0.5.2 71 | pycocotools==2.0 72 | pycrypto==2.6.1 73 | pycups==1.9.73 74 | Pygments==2.2.0 75 | pygobject==3.24.1 76 | pyparsing==2.1.10 77 | pyproj==1.9.5.1 78 | python-apt==1.4.0b3 79 | python-dateutil==2.6.1 80 | python-debian==0.1.30 81 | pytz==2017.2 82 | PyWavelets==0.5.1 83 | pyxdg==0.25 84 | PyYAML==3.12 85 | pyzmq==16.0.3 86 | qtconsole==4.3.1 87 | reportlab==3.4.0 88 | requests==2.18.1 89 | scikit-image==0.13.0 90 | scipy==0.18.1 91 | screen-resolution-extra==0.0.0 92 | SecretStorage==2.3.1 93 | Shapely==1.6.2.post1 94 | simplegeneric==0.8.1 95 | simplejson==3.11.1 96 | six==1.11.0 97 | system-service==0.3 98 | systemd-python==234 99 | tensorflow-gpu==1.3.0 100 | tensorflow-tensorboard==0.1.8 101 | terminado==0.6 102 | testpath==0.3.1 103 | tornado==4.5.2 104 | traitlets==4.3.2 105 | ubuntu-drivers-common==0.0.0 106 | ufw==0.35 107 | unattended-upgrades==0.1 108 | urllib3==1.21.1 109 | usb-creator==0.3.3 110 | virtualenv==15.1.0 111 | wadllib==1.3.2 112 | wcwidth==0.1.7 113 | Werkzeug==0.12.2 114 | widgetsnbextension==3.0.7 115 | xkit==0.0.0 116 | zope.interface==4.3.2 117 | -------------------------------------------------------------------------------- /reviewosmanomaly.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import glob 4 | import imagestoosm.config as osmcfg 5 | import xml.etree.ElementTree as ET 6 | import QuadKey.quadkey as quadkey 7 | import shapely.geometry as geometry 8 | import matplotlib.pyplot as plt 9 | import skimage.io 10 | import shutil 11 | 12 | def _find_getch(): 13 | try: 14 | import termios 15 | except ImportError: 16 | # Non-POSIX. Return msvcrt's (Windows') getch. 17 | import msvcrt 18 | return msvcrt.getch 19 | 20 | # POSIX system. Create and return a getch that manipulates the tty. 21 | import sys, tty 22 | def _getch(): 23 | fd = sys.stdin.fileno() 24 | old_settings = termios.tcgetattr(fd) 25 | try: 26 | tty.setraw(fd) 27 | ch = sys.stdin.read(1) 28 | finally: 29 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 30 | return ch 31 | 32 | return _getch 33 | 34 | getch = _find_getch() 35 | 36 | 37 | addDirectory = os.path.join( "anomaly","add","*.osm") 38 | anomalyStatusFile = os.path.join( "anomaly","status.csv") 39 | 40 | # read in OSM files, convert to pixels z18, make them shapely polygons 41 | 42 | newWays = {} 43 | for osmFileName in glob.glob(addDirectory): 44 | 45 | (path,filename) = os.path.split(osmFileName); 46 | wayNumber = os.path.splitext(filename)[0] 47 | 48 | newEntry = { 49 | "imageName" : os.path.join( path,str(wayNumber) + ".jpg") , 50 | "osmFile" : osmFileName, 51 | "tags" : {}, 52 | "status" : "" 53 | } 54 | 55 | tree = ET.parse(osmFileName) 56 | root = tree.getroot() 57 | 58 | for tag in root.findall('./way/tag'): 59 | key = tag.attrib["k"] 60 | val = tag.attrib["v"] 61 | 62 | newEntry['tags'][key] = val 63 | 64 | pts =[] 65 | for node in root.iter('node'): 66 | pt = ( float(node.attrib['lat']),float(node.attrib['lon'])) 67 | pixel = quadkey.TileSystem.geo_to_pixel(pt,osmcfg.tileZoom) 68 | pts.append(pixel) 69 | 70 | if ( len(pts ) > 2 ): 71 | newEntry["geometry"] = geometry.Polygon(pts) 72 | #print(newEntry ) 73 | newWays[osmFileName] = newEntry 74 | 75 | # read in review file, file status (accepted or rejected), path to osm 76 | if ( os.path.exists(anomalyStatusFile)): 77 | with open(anomalyStatusFile,"rt",encoding="ascii") as f: 78 | for line in f: 79 | (status,osmFileName) = line.split(',') 80 | osmFileName = osmFileName.strip() 81 | newWays[osmFileName]['status'] = status 82 | 83 | fig = plt.figure() 84 | 85 | for wayKey in sorted(newWays): 86 | # if reviewed skip 87 | way = newWays[wayKey] 88 | 89 | if ( len(way['status']) == 0 ) : 90 | 91 | # for each way, check for overlap, same tags, add to review cluster, handle more ways than maxSubPlots 92 | subPlotCols = 2 93 | subPlotRows = 2 94 | maxSubPlots = subPlotCols*subPlotRows 95 | 96 | reviewSet = [way] 97 | for otherKey in sorted(newWays): 98 | other = newWays[otherKey] 99 | if ( other != way and len(other['status']) == 0 and way['tags'] == other['tags'] and other['geometry'].intersects( way['geometry'])): 100 | reviewSet.append(other) 101 | 102 | for wayIndex in range(len(reviewSet)): 103 | other = reviewSet[wayIndex] 104 | other['status'] = 'rejected' 105 | 106 | acceptedWay = {} 107 | viewSet = [] 108 | for wayIndex in range(len(reviewSet)): 109 | viewSet.append(reviewSet[wayIndex]) 110 | 111 | if ( len(viewSet) == maxSubPlots or wayIndex+1 >= len(reviewSet)): 112 | 113 | for plotIndex in range(maxSubPlots): 114 | sb = fig.add_subplot(subPlotRows,subPlotCols,plotIndex+1) 115 | sb.cla() 116 | 117 | for wayIndex in range(len(viewSet)): 118 | fig.add_subplot(subPlotRows,subPlotCols,wayIndex+1) 119 | plt.title("{} {}".format(wayIndex+1,viewSet[wayIndex]['osmFile'])) 120 | image = skimage.io.imread( viewSet[wayIndex]['imageName']) 121 | plt.imshow(image) 122 | 123 | plt.show(block=False) 124 | plt.pause(0.05) 125 | 126 | goodInput = False 127 | while goodInput == False: 128 | print("{} - q to quit, 0 to reject all, to except use sub plot index".format(way['osmFile'])) 129 | c = getch() 130 | 131 | try: 132 | index = int(c) 133 | if ( index > 0): 134 | acceptedWay = viewSet[index-1 ] 135 | viewSet = [acceptedWay] 136 | print("selected {} {}".format(acceptedWay['osmFile'],index)) 137 | goodInput = True 138 | if ( index == 0): 139 | viewSet = [reviewSet[0]] 140 | acceptedWay = {} 141 | print("reject all") 142 | goodInput = True 143 | 144 | except: 145 | if ( c == "q"): 146 | sys.exit(0) 147 | print("what??") 148 | 149 | if ( bool(acceptedWay) ) : 150 | acceptedWay['status'] = 'accepted' 151 | print("accepted {}".format(acceptedWay['osmFile'])) 152 | 153 | if ( os.path.exists(anomalyStatusFile+".1")): 154 | shutil.copy(anomalyStatusFile+".1",anomalyStatusFile+".2" ) 155 | if ( os.path.exists(anomalyStatusFile)): 156 | shutil.copy(anomalyStatusFile,anomalyStatusFile+".1" ) 157 | 158 | with open(anomalyStatusFile,"wt",encoding="ascii") as f: 159 | for otherKey in sorted(newWays): 160 | other = newWays[otherKey] 161 | if ( len(other['status']) > 0 ): 162 | f.write("{},{}\n".format(other['status'],other['osmFile'])) 163 | 164 | -------------------------------------------------------------------------------- /sample-images/phase1-baseball-outfieds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jremillard/images-to-osm/5375d69c0f78aba9c153b0ada7ef7d6b8fb87b13/sample-images/phase1-baseball-outfieds.png -------------------------------------------------------------------------------- /sample-images/sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jremillard/images-to-osm/5375d69c0f78aba9c153b0ada7ef7d6b8fb87b13/sample-images/sample1.png -------------------------------------------------------------------------------- /sample-images/sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jremillard/images-to-osm/5375d69c0f78aba9c153b0ada7ef7d6b8fb87b13/sample-images/sample2.png -------------------------------------------------------------------------------- /sample-images/sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jremillard/images-to-osm/5375d69c0f78aba9c153b0ada7ef7d6b8fb87b13/sample-images/sample3.png -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | 2 | # coding: utf-8 3 | 4 | # hacked up from the shapes example file. 5 | 6 | import sys 7 | sys.path.append("Mask_RCNN") 8 | 9 | import os 10 | import random 11 | import math 12 | import time 13 | import numpy as np 14 | import random 15 | import glob 16 | import skimage 17 | import osmmodelconfig 18 | 19 | from config import Config 20 | import imagestoosm.config as osmcfg 21 | import utils 22 | import model as modellib 23 | from model import log 24 | 25 | # Root directory of the project 26 | #ROOT_DIR = os.getcwd() 27 | ROOT_DIR = os.path.dirname(os.path.realpath(sys.argv[0])) 28 | 29 | # Directory to save logs and trained model 30 | MODEL_DIR = os.path.join(ROOT_DIR, "logs") 31 | 32 | # Path to COCO trained weights 33 | COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") 34 | 35 | config = osmmodelconfig.OsmModelConfig() 36 | config.ROOT_DIR = ROOT_DIR 37 | config.display() 38 | 39 | fullTrainingDir = os.path.join( ROOT_DIR, osmcfg.trainDir,"*") 40 | fullImageList = [] 41 | for imageDir in glob.glob(fullTrainingDir): 42 | if ( os.path.isdir( os.path.join( fullTrainingDir, imageDir) )): 43 | id = os.path.split(imageDir)[1] 44 | fullImageList.append( id) 45 | 46 | random.shuffle(fullImageList) 47 | 48 | cutoffIndex = int(len(fullImageList)*.75) 49 | trainingImages = fullImageList[0:cutoffIndex ] 50 | validationImages = fullImageList[cutoffIndex:-1 ] 51 | 52 | # Training dataset 53 | dataset_train = osmmodelconfig.OsmImagesDataset(ROOT_DIR) 54 | dataset_train.load(trainingImages, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1]) 55 | dataset_train.prepare() 56 | 57 | # Validation dataset 58 | dataset_val = osmmodelconfig.OsmImagesDataset(ROOT_DIR) 59 | dataset_val.load(validationImages, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1]) 60 | dataset_val.prepare() 61 | 62 | # Create model in training mode 63 | model = modellib.MaskRCNN(mode="training", config=config, 64 | model_dir=MODEL_DIR) 65 | 66 | 67 | # Which weights to start with? 68 | init_with = "coco" # imagenet, coco, or last 69 | 70 | if init_with == "imagenet": 71 | model.load_weights(model.get_imagenet_weights(), by_name=True) 72 | elif init_with == "coco": 73 | # Load weights trained on MS COCO, but skip layers that 74 | # are different due to the different number of classes 75 | # See README for instructions to download the COCO weights 76 | model.load_weights(COCO_MODEL_PATH, by_name=True, 77 | exclude=["mrcnn_class_logits", "mrcnn_bbox_fc", 78 | "mrcnn_bbox", "mrcnn_mask"]) 79 | elif init_with == "last": 80 | # Load the last model you trained and continue training 81 | print(model.find_last()[1]) 82 | model.load_weights(model.find_last()[1], by_name=True) 83 | 84 | if ( init_with != "last") : 85 | # Training - Stage 1 86 | # Adjust epochs and layers as needed 87 | print("Training network heads") 88 | model.train(dataset_train, dataset_val, 89 | learning_rate=config.LEARNING_RATE, 90 | epochs=10, 91 | layers='heads') 92 | 93 | # Training - Stage 2 94 | # Finetune layers from ResNet stage 4 and up 95 | print("Training Resnet layer 3+") 96 | model.train(dataset_train, dataset_val, 97 | learning_rate=config.LEARNING_RATE/10, 98 | epochs=100, 99 | layers='3+') 100 | 101 | # Finetune layers from ResNet stage 3 and up 102 | print("Training all") 103 | model.train(dataset_train, dataset_val, 104 | learning_rate=config.LEARNING_RATE / 100, 105 | epochs=1000, 106 | layers='all') 107 | 108 | -------------------------------------------------------------------------------- /train_shapes.py: -------------------------------------------------------------------------------- 1 | 2 | # coding: utf-8 3 | 4 | # # Mask R-CNN - Train on Shapes Dataset 5 | # 6 | # 7 | # This notebook shows how to train Mask R-CNN on your own dataset. To keep things simple we use a synthetic dataset of shapes (squares, triangles, and circles) which enables fast training. You'd still need a GPU, though, because the network backbone is a Resnet101, which would be too slow to train on a CPU. On a GPU, you can start to get okay-ish results in a few minutes, and good results in less than an hour. 8 | # 9 | # The code of the *Shapes* dataset is included below. It generates images on the fly, so it doesn't require downloading any data. And it can generate images of any size, so we pick a small image size to train faster. 10 | 11 | # In[1]: 12 | 13 | import sys 14 | sys.path.append("Mask_RCNN") 15 | 16 | import os 17 | import random 18 | import math 19 | import re 20 | import time 21 | import numpy as np 22 | import cv2 23 | import matplotlib 24 | import matplotlib.pyplot as plt 25 | 26 | from config import Config 27 | import utils 28 | import model as modellib 29 | import visualize 30 | from model import log 31 | 32 | # Root directory of the project 33 | ROOT_DIR = os.getcwd() 34 | 35 | # Directory to save logs and trained model 36 | MODEL_DIR = os.path.join(ROOT_DIR, "logs") 37 | 38 | # Path to COCO trained weights 39 | COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5") 40 | 41 | 42 | # ## Configurations 43 | 44 | # In[2]: 45 | 46 | 47 | class ShapesConfig(Config): 48 | """Configuration for training on the toy shapes dataset. 49 | Derives from the base Config class and overrides values specific 50 | to the toy shapes dataset. 51 | """ 52 | # Give the configuration a recognizable name 53 | NAME = "shapes" 54 | 55 | # Train on 1 GPU and 8 images per GPU. We can put multiple images on each 56 | # GPU because the images are small. Batch size is 8 (GPUs * images/GPU). 57 | GPU_COUNT = 1 58 | IMAGES_PER_GPU = 8 59 | 60 | # Number of classes (including background) 61 | NUM_CLASSES = 1 + 3 # background + 3 shapes 62 | 63 | # Use small images for faster training. Set the limits of the small side 64 | # the large side, and that determines the image shape. 65 | IMAGE_MIN_DIM = 128 66 | IMAGE_MAX_DIM = 128 67 | 68 | # Use smaller anchors because our image and objects are small 69 | RPN_ANCHOR_SCALES = (8, 16, 32, 64, 128) # anchor side in pixels 70 | 71 | # Reduce training ROIs per image because the images are small and have 72 | # few objects. Aim to allow ROI sampling to pick 33% positive ROIs. 73 | TRAIN_ROIS_PER_IMAGE = 32 74 | 75 | # Use a small epoch since the data is simple 76 | STEPS_PER_EPOCH = 100 77 | 78 | # use small validation steps since the epoch is small 79 | VALIDATION_STEPS = 5 80 | 81 | config = ShapesConfig() 82 | config.display() 83 | 84 | 85 | # ## Notebook Preferences 86 | 87 | # In[3]: 88 | 89 | 90 | def get_ax(rows=1, cols=1, size=8): 91 | """Return a Matplotlib Axes array to be used in 92 | all visualizations in the notebook. Provide a 93 | central point to control graph sizes. 94 | 95 | Change the default size attribute to control the size 96 | of rendered images 97 | """ 98 | _, ax = plt.subplots(rows, cols, figsize=(size*cols, size*rows)) 99 | return ax 100 | 101 | 102 | # ## Dataset 103 | # 104 | # Create a synthetic dataset 105 | # 106 | # Extend the Dataset class and add a method to load the shapes dataset, `load_shapes()`, and override the following methods: 107 | # 108 | # * load_image() 109 | # * load_mask() 110 | # * image_reference() 111 | 112 | # In[4]: 113 | 114 | 115 | class ShapesDataset(utils.Dataset): 116 | """Generates the shapes synthetic dataset. The dataset consists of simple 117 | shapes (triangles, squares, circles) placed randomly on a blank surface. 118 | The images are generated on the fly. No file access required. 119 | """ 120 | 121 | def load_shapes(self, count, height, width): 122 | """Generate the requested number of synthetic images. 123 | count: number of images to generate. 124 | height, width: the size of the generated images. 125 | """ 126 | # Add classes 127 | self.add_class("shapes", 1, "square") 128 | self.add_class("shapes", 2, "circle") 129 | self.add_class("shapes", 3, "triangle") 130 | 131 | # Add images 132 | # Generate random specifications of images (i.e. color and 133 | # list of shapes sizes and locations). This is more compact than 134 | # actual images. Images are generated on the fly in load_image(). 135 | for i in range(count): 136 | bg_color, shapes = self.random_image(height, width) 137 | self.add_image("shapes", image_id=i, path=None, 138 | width=width, height=height, 139 | bg_color=bg_color, shapes=shapes) 140 | 141 | def load_image(self, image_id): 142 | """Generate an image from the specs of the given image ID. 143 | Typically this function loads the image from a file, but 144 | in this case it generates the image on the fly from the 145 | specs in image_info. 146 | """ 147 | info = self.image_info[image_id] 148 | bg_color = np.array(info['bg_color']).reshape([1, 1, 3]) 149 | image = np.ones([info['height'], info['width'], 3], dtype=np.uint8) 150 | image = image * bg_color.astype(np.uint8) 151 | for shape, color, dims in info['shapes']: 152 | image = self.draw_shape(image, shape, dims, color) 153 | return image 154 | 155 | def image_reference(self, image_id): 156 | """Return the shapes data of the image.""" 157 | info = self.image_info[image_id] 158 | if info["source"] == "shapes": 159 | return info["shapes"] 160 | else: 161 | super(self.__class__).image_reference(self, image_id) 162 | 163 | def load_mask(self, image_id): 164 | """Generate instance masks for shapes of the given image ID. 165 | """ 166 | info = self.image_info[image_id] 167 | shapes = info['shapes'] 168 | count = len(shapes) 169 | mask = np.zeros([info['height'], info['width'], count], dtype=np.uint8) 170 | for i, (shape, _, dims) in enumerate(info['shapes']): 171 | mask[:, :, i:i+1] = self.draw_shape(mask[:, :, i:i+1].copy(), 172 | shape, dims, 1) 173 | # Handle occlusions 174 | occlusion = np.logical_not(mask[:, :, -1]).astype(np.uint8) 175 | for i in range(count-2, -1, -1): 176 | mask[:, :, i] = mask[:, :, i] * occlusion 177 | occlusion = np.logical_and(occlusion, np.logical_not(mask[:, :, i])) 178 | # Map class names to class IDs. 179 | class_ids = np.array([self.class_names.index(s[0]) for s in shapes]) 180 | return mask, class_ids.astype(np.int32) 181 | 182 | def draw_shape(self, image, shape, dims, color): 183 | """Draws a shape from the given specs.""" 184 | # Get the center x, y and the size s 185 | x, y, s = dims 186 | if shape == 'square': 187 | cv2.rectangle(image, (x-s, y-s), (x+s, y+s), color, -1) 188 | elif shape == "circle": 189 | cv2.circle(image, (x, y), s, color, -1) 190 | elif shape == "triangle": 191 | points = np.array([[(x, y-s), 192 | (x-s/math.sin(math.radians(60)), y+s), 193 | (x+s/math.sin(math.radians(60)), y+s), 194 | ]], dtype=np.int32) 195 | cv2.fillPoly(image, points, color) 196 | return image 197 | 198 | def random_shape(self, height, width): 199 | """Generates specifications of a random shape that lies within 200 | the given height and width boundaries. 201 | Returns a tuple of three valus: 202 | * The shape name (square, circle, ...) 203 | * Shape color: a tuple of 3 values, RGB. 204 | * Shape dimensions: A tuple of values that define the shape size 205 | and location. Differs per shape type. 206 | """ 207 | # Shape 208 | shape = random.choice(["square", "circle", "triangle"]) 209 | # Color 210 | color = tuple([random.randint(0, 255) for _ in range(3)]) 211 | # Center x, y 212 | buffer = 20 213 | y = random.randint(buffer, height - buffer - 1) 214 | x = random.randint(buffer, width - buffer - 1) 215 | # Size 216 | s = random.randint(buffer, height//4) 217 | return shape, color, (x, y, s) 218 | 219 | def random_image(self, height, width): 220 | """Creates random specifications of an image with multiple shapes. 221 | Returns the background color of the image and a list of shape 222 | specifications that can be used to draw the image. 223 | """ 224 | # Pick random background color 225 | bg_color = np.array([random.randint(0, 255) for _ in range(3)]) 226 | # Generate a few random shapes and record their 227 | # bounding boxes 228 | shapes = [] 229 | boxes = [] 230 | N = random.randint(1, 4) 231 | for _ in range(N): 232 | shape, color, dims = self.random_shape(height, width) 233 | shapes.append((shape, color, dims)) 234 | x, y, s = dims 235 | boxes.append([y-s, x-s, y+s, x+s]) 236 | # Apply non-max suppression wit 0.3 threshold to avoid 237 | # shapes covering each other 238 | keep_ixs = utils.non_max_suppression(np.array(boxes), np.arange(N), 0.3) 239 | shapes = [s for i, s in enumerate(shapes) if i in keep_ixs] 240 | return bg_color, shapes 241 | 242 | 243 | # In[5]: 244 | 245 | 246 | # Training dataset 247 | dataset_train = ShapesDataset() 248 | dataset_train.load_shapes(500, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1]) 249 | dataset_train.prepare() 250 | 251 | # Validation dataset 252 | dataset_val = ShapesDataset() 253 | dataset_val.load_shapes(50, config.IMAGE_SHAPE[0], config.IMAGE_SHAPE[1]) 254 | dataset_val.prepare() 255 | 256 | 257 | # In[6]: 258 | 259 | 260 | # Load and display random samples 261 | #image_ids = np.random.choice(dataset_train.image_ids, 4) 262 | #for image_id in image_ids: 263 | # image = dataset_train.load_image(image_id) 264 | # mask, class_ids = dataset_train.load_mask(image_id) 265 | # visualize.display_top_masks(image, mask, class_ids, dataset_train.class_names) 266 | 267 | 268 | # ## Ceate Model 269 | 270 | # In[7]: 271 | 272 | 273 | # Create model in training mode 274 | model = modellib.MaskRCNN(mode="training", config=config, 275 | model_dir=MODEL_DIR) 276 | 277 | 278 | # In[8]: 279 | 280 | 281 | # Which weights to start with? 282 | init_with = "coco" # imagenet, coco, or last 283 | 284 | if init_with == "imagenet": 285 | model.load_weights(model.get_imagenet_weights(), by_name=True) 286 | elif init_with == "coco": 287 | # Load weights trained on MS COCO, but skip layers that 288 | # are different due to the different number of classes 289 | # See README for instructions to download the COCO weights 290 | model.load_weights(COCO_MODEL_PATH, by_name=True, 291 | exclude=["mrcnn_class_logits", "mrcnn_bbox_fc", 292 | "mrcnn_bbox", "mrcnn_mask"]) 293 | elif init_with == "last": 294 | # Load the last model you trained and continue training 295 | model.load_weights(model.find_last()[1], by_name=True) 296 | 297 | 298 | # ## Training 299 | # 300 | # Train in two stages: 301 | # 1. Only the heads. Here we're freezing all the backbone layers and training only the randomly initialized layers (i.e. the ones that we didn't use pre-trained weights from MS COCO). To train only the head layers, pass `layers='heads'` to the `train()` function. 302 | # 303 | # 2. Fine-tune all layers. For this simple example it's not necessary, but we're including it to show the process. Simply pass `layers="all` to train all layers. 304 | 305 | # In[9]: 306 | 307 | 308 | # Train the head branches 309 | # Passing layers="heads" freezes all layers except the head 310 | # layers. You can also pass a regular expression to select 311 | # which layers to train by name pattern. 312 | model.train(dataset_train, dataset_val, 313 | learning_rate=config.LEARNING_RATE, 314 | epochs=20, 315 | layers='heads') 316 | 317 | 318 | # In[ ]: 319 | 320 | 321 | # Fine tune all layers 322 | # Passing layers="all" trains all layers. You can also 323 | # pass a regular expression to select which layers to 324 | # train by name pattern. 325 | #model.train(dataset_train, dataset_val, 326 | # learning_rate=config.LEARNING_RATE / 10, 327 | # epochs=2, 328 | # layers="all") 329 | 330 | 331 | # In[ ]: 332 | 333 | 334 | # Save weights 335 | # Typically not needed because callbacks save after every epoch 336 | # Uncomment to save manually 337 | # model_path = os.path.join(MODEL_DIR, "mask_rcnn_shapes.h5") 338 | # model.keras_model.save_weights(model_path) 339 | 340 | 341 | # ## Detection 342 | 343 | # In[ ]: 344 | 345 | 346 | class InferenceConfig(ShapesConfig): 347 | GPU_COUNT = 1 348 | IMAGES_PER_GPU = 1 349 | 350 | inference_config = InferenceConfig() 351 | 352 | # Recreate the model in inference mode 353 | model = modellib.MaskRCNN(mode="inference", 354 | config=inference_config, 355 | model_dir=MODEL_DIR) 356 | 357 | # Get path to saved weights 358 | # Either set a specific path or find last trained weights 359 | # model_path = os.path.join(ROOT_DIR, ".h5 file name here") 360 | model_path = model.find_last()[1] 361 | 362 | # Load trained weights (fill in path to trained weights here) 363 | assert model_path != "", "Provide path to trained weights" 364 | print("Loading weights from ", model_path) 365 | model.load_weights(model_path, by_name=True) 366 | 367 | 368 | # In[ ]: 369 | 370 | 371 | # Test on a random image 372 | image_id = random.choice(dataset_val.image_ids) 373 | original_image, image_meta, gt_class_id, gt_bbox, gt_mask = modellib.load_image_gt(dataset_val, inference_config, 374 | image_id, use_mini_mask=False) 375 | 376 | log("original_image", original_image) 377 | log("image_meta", image_meta) 378 | log("gt_class_id", gt_bbox) 379 | log("gt_bbox", gt_bbox) 380 | log("gt_mask", gt_mask) 381 | 382 | visualize.display_instances(original_image, gt_bbox, gt_mask, gt_class_id, 383 | dataset_train.class_names, figsize=(8, 8)) 384 | 385 | 386 | # In[ ]: 387 | 388 | 389 | results = model.detect([original_image], verbose=1) 390 | 391 | r = results[0] 392 | visualize.display_instances(original_image, r['rois'], r['masks'], r['class_ids'], 393 | dataset_val.class_names, r['scores'], ax=get_ax()) 394 | 395 | 396 | # ## Evaluation 397 | 398 | # In[ ]: 399 | 400 | 401 | # Compute VOC-Style mAP @ IoU=0.5 402 | # Running on 10 images. Increase for better accuracy. 403 | image_ids = np.random.choice(dataset_val.image_ids, 10) 404 | APs = [] 405 | for image_id in image_ids: 406 | # Load image and ground truth data 407 | image, image_meta, gt_class_id, gt_bbox, gt_mask = modellib.load_image_gt(dataset_val, inference_config, 408 | image_id, use_mini_mask=False) 409 | molded_images = np.expand_dims(modellib.mold_image(image, inference_config), 0) 410 | # Run object detection 411 | results = model.detect([image], verbose=0) 412 | r = results[0] 413 | # Compute AP 414 | AP, precisions, recalls, overlaps = utils.compute_ap(gt_bbox, gt_class_id, 415 | r["rois"], r["class_ids"], r["scores"]) 416 | APs.append(AP) 417 | 418 | print("mAP: ", np.mean(APs)) 419 | 420 | -------------------------------------------------------------------------------- /trainall.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.system("python getdatafromosm.py") 4 | #os.system("python gettilesfrombing.py 2>&1 | tee all.log") 5 | os.system("python maketrainingimages.py 2>&1 | tee -a all.log") 6 | os.system("python train.py 2>&1 | tee -a all.log") 7 | --------------------------------------------------------------------------------