├── AUTHORS ├── CODEOWNERS ├── LICENSE ├── LICENSE-CC BY-NC-ND 4.0.markdown ├── README.md ├── __init__.py ├── aug_util.py ├── create_geojson_basedon_foler.py ├── create_toy_valdata.py ├── delete_bad_labels.py ├── geotiff_to_tiff.py ├── get_data_stat.py ├── identify_bad_labels.py ├── inference ├── .DS_Store ├── .create_detections.py.swo ├── .create_detections.py.swp ├── LICENSE ├── README.md ├── __init__.py ├── __pycache__ │ └── det_util.cpython-36.pyc ├── create_detections.py ├── create_detections.sh ├── det_util.py ├── harvey_label_map_2class.pbtxt ├── harvey_label_map_first.pbtxt └── single_detection.sh ├── plot_bbox.py ├── plot_bbox_uid_small.py ├── process_wv_noaa_test.py ├── process_wv_noaa_train_2class.py ├── process_wv_tomnod_test_1class.py ├── process_wv_tomnod_test_2class.py ├── process_wv_tomnod_train_1class.py ├── process_wv_tomnod_train_2class.py ├── scoring ├── Dockerfile ├── __pycache__ │ ├── matching.cpython-36.pyc │ └── rectangle.cpython-36.pyc ├── evaluation.py ├── matching.py ├── rectangle.py ├── requirements.txt ├── run_scoring.sh ├── score.bash └── score.py ├── selective_copy.py ├── split_geojson.py ├── tfr_util.py ├── tomnod_vis ├── .DS_Store ├── blue-tarp.jpg ├── flooded.jpg ├── flooded2.jpg ├── flooded3.jpg ├── flooded_large.png └── roof-damage.jpg ├── train_test_split.py └── wv_util.py /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as: 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | eScience Insititute at University of Washington 10 | An Yan 11 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | All code in this repository was developed by @annieyan based on 2 | the code for the DIUx xView Detection Challenge (https://github.com/DIUx-xView) 3 | for eScience Institue at University of Washington 2018 Data Science for Social Good 4 | Project (https://escience.washington.edu/2018-data-science-for-social-good-projects/) 5 | -------------------------------------------------------------------------------- /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 2018, The Authors. 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. 202 | -------------------------------------------------------------------------------- /LICENSE-CC BY-NC-ND 4.0.markdown: -------------------------------------------------------------------------------- 1 | ## creative commons 2 | 3 | # Attribution-NonCommercial-NoDerivatives 4.0 International 4 | 5 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 6 | 7 | ### Using Creative Commons Public Licenses 8 | 9 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 10 | 11 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 12 | 13 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 14 | 15 | ## Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License 16 | 17 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 18 | 19 | ### Section 1 – Definitions. 20 | 21 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 22 | 23 | b. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 24 | 25 | e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 26 | 27 | f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 28 | 29 | h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 30 | 31 | i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 32 | 33 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 34 | 35 | i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 36 | 37 | j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 38 | 39 | k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 40 | 41 | l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 42 | 43 | ### Section 2 – Scope. 44 | 45 | a. ___License grant.___ 46 | 47 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 48 | 49 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 50 | 51 | B. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only. 52 | 53 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 54 | 55 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 56 | 57 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 58 | 59 | 5. __Downstream recipients.__ 60 | 61 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 62 | 63 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 64 | 65 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 66 | 67 | b. ___Other rights.___ 68 | 69 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 70 | 71 | 2. Patent and trademark rights are not licensed under this Public License. 72 | 73 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 74 | 75 | ### Section 3 – License Conditions. 76 | 77 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 78 | 79 | a. ___Attribution.___ 80 | 81 | 1. If You Share the Licensed Material, You must: 82 | 83 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 84 | 85 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 86 | 87 | ii. a copyright notice; 88 | 89 | iii. a notice that refers to this Public License; 90 | 91 | iv. a notice that refers to the disclaimer of warranties; 92 | 93 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 94 | 95 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 96 | 97 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 98 | 99 | For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. 100 | 101 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 102 | 103 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 104 | 105 | ### Section 4 – Sui Generis Database Rights. 106 | 107 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 108 | 109 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material; 110 | 111 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 112 | 113 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 114 | 115 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 116 | 117 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 118 | 119 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 120 | 121 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 122 | 123 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 124 | 125 | ### Section 6 – Term and Termination. 126 | 127 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 128 | 129 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 130 | 131 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 132 | 133 | 2. upon express reinstatement by the Licensor. 134 | 135 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 136 | 137 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 138 | 139 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 140 | 141 | ### Section 7 – Other Terms and Conditions. 142 | 143 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 144 | 145 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 146 | 147 | ### Section 8 – Interpretation. 148 | 149 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 150 | 151 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 152 | 153 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 154 | 155 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 156 | 157 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 158 | > 159 | > Creative Commons may be contacted at creativecommons.org 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Disclaimer** 2 | 3 | This repository is dual-licensed under Apache 2.0 and Creative Common Attribution-NonCommercial-NoDerivs CC BY-NC-ND 4.0 (see license files attached in this repository). 4 | 5 | This repository is modified from the code for the [ the DIUx xView Detection Challenge](https://github.com/DIUx-xView). The paper is available [here](https://arxiv.org/abs/1802.07856). 6 | 7 | This repository is created for [Automatic Damage Annotation on Post-Hurricane Satellite Imagery](https://dds-lab.github.io/disaster-damage-detection/), one of three [projects](https://escience.washington.edu/2018-data-science-for-social-good-projects/) from the 2018 Data Science for Social Good summer fellowship at the University of Washington eScience Institute. 8 | 9 | * Project Lead: Professor Youngjun Choe 10 | * Data Scientist: Valentina Staneva 11 | * Dataset Creation: Sean Chen, Andrew Escay, Chris Haberland, Tessa Schneider, An Yan 12 | * Data processing for training, model training and inference, experiment design: An Yan 13 | 14 | 15 | # Introduction 16 | 17 | Two object detection algorithms, [Single Shot Multibox Detector](https://arxiv.org/abs/1512.02325) and [Faster R-CNN](https://arxiv.org/abs/1506.01497) were applied to satellite imagery for hurricane Harvey provided by [DigitalGlobe Open Data Program](https://www.digitalglobe.com/opendata) and crowd-sourced damaged buildings labels provided by [Tomnod](https://www.tomnod.com/). Our team built dataset for damaged building object detection by placing bounding boxes for damaged buildings whose locations are labelled by Tomnod. For more information about dataset creation, please visit [our website](https://dds-lab.github.io/disaster-damage-detection/data-collecting/). 18 | 19 | We used[tensorflow object detection API](https://github.com/tensorflow/models) to run SSD and Faster R-CNN. We used a baseline model provided by [xView Challenge](https://github.com/DIUx-xView/) as pre-trained model for SSD. This repository contains code performs data processing training, specifically, 1) image chipping, 2) bounding box visualization, 3) train-test split, 4) data augmentation, 5) convert imagery and labels into TFrecord, 6) inference, 7) scoring, 8) cloud removal, 9) black region removal, etc. See [here](https://github.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/wiki/Workflow). We fed the data into SSD and Faster R-CNN and predicted the bounding boxes for damaged and non-damaged buildings. 20 | 21 | 22 | # Data Processing for training 23 | 24 | The dataset contains 875 satellite imagery of the size 2048 x 2048 in geotiff format. Each of the imagery contains at least one bounding box. The dataset also includes a geojson file containing bounding box location and its class (damaged building or non-damaged building). The dataset contains many invalid bounding boxes that do not cover any building, for example, bounding boxes over the cloud or in the field. Automatic cloud removal method was applied and followed by manual cleaning. Data was then split into training, validation, and test set. 25 | 26 | Imagery were converted to tiff files and chipped into smaller non-overlapping images for training. Different sizes (500 x 500, 300 x 300, and 200 x 200) were experimented and 200 x 200 was chosen because it resulted in better performance. This may because 200 x 200 chips contains less context and buildings appear larger in resultant images than other two sizes. Bounding boxes cross multiple chips were truncated at the edge, among them, those with small width ( < 30 pixels) were discarded. Chips with black region area larger than 10% of its coverage were discarded. Only chips with at least one building were written into TF-record. 27 | 28 | Three training datasets were created with different treatments: 1) Damaged-Only: with only bounding boxes over damaged buildings whose labels came from Tomnod labels; 2) Damaged + non-damaged: bounding boxes of all buildings in the study area were created from building footprints. Buildings without Tomnod labels were treated as another class as non-damaged labels; 3) Augmented damaged and non-damaged: There are about 10 times more non-damaged buildings than damaged buildings. Chips with damaged buildings were then augmented using different random combinations of rotation, flip, zoom-in, Gaussian-blur, Gaussian-noise, change contrast, and change brightness. Additional 200 x 200 chips were created with randomly chosen damaged buildings bounding boxes in the center as a substitute of translation. For details, see [here](https://github.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/wiki/Clean-Data-Run-Statistics-----Harvey). 29 | 30 | Then the chips and its associated labels went though optional processing (i.e., data augmentation) and were converted to TFrecord ready for training and testing. 31 | 32 | # Experiments 33 | 34 | ### SSD 35 | 36 | Inception v2 was chosen as the backbone network for SSD. A single GPU (Tesla K80) machine on AWS was used for model training. Three experiments were conducted using three different training datasets described above. The results are shown in Table 1. 37 | 38 | Table 1, Average Precision (AP) Scores for Each Model Run 39 | 40 | | | damaged/flooded building | non-damaged building | mAP | 41 | |--------------------------------------|------------------|----------------------|-------------| 42 | | damaged only | 0.0523 | NA | 0.0523 | 43 | | damaged + non-damaged | 0.3352 | 0.5703 | 0.4528 | 44 | | damaged + non-damaged + augmentation | 0.4742 | 0.6223 | 0.5483 | 45 | 46 | 47 | 48 | Training on 2-class produces a far better model than using only damaged building bounding boxes. This may because adding non-damaged buildings data increases model's overall capability to identify buildings, either damaged or non-damaged. Data augmentation helped boosting performance for both damaged and non-damaged buildings. Visualizations of inference results from "Augmented damaged + non-damaged" model are shown in the figures below. 49 | 50 | 51 | ![flooded_large](https://github.com/DDS-Lab/harvey_data_process/blob/master/tomnod_vis/flooded_large.png) 52 |
Predicted damaged/flooded buildings (red) and non-damaged buildings (green)
53 | 54 | 55 | ![](https://github.com/DDS-Lab/harvey_data_process/blob/master/tomnod_vis/blue-tarp.jpg) 56 | ![](https://github.com/DDS-Lab/harvey_data_process/blob/master/tomnod_vis/roof-damage.jpg) 57 | ![](https://github.com/DDS-Lab/harvey_data_process/blob/master/tomnod_vis/flooded2.jpg) 58 | ![](https://github.com/DDS-Lab/harvey_data_process/blob/master/tomnod_vis/flooded3.jpg) 59 | 60 |
Comparison between ground truth (left panel, '1' denotes damaged/flooded buildings, '2' denotes non-damaged buildings) and predictions (right panel, green denotes damaged buildings and teal denotes non-damaged buildings). The model is able to pick different damaged types including flooded building, blue tarp on the roof, and crashed buildings.
61 | 62 | 63 | ### Faster R-CNN 64 | Faster R-CNN with inception v2 backbone network was applied to the "Augmented damaged + non-damaged" training dataset. Average Precision is 0.31 for damaged/flooded buildings, 0.61 for non-damaged buildings, and 0.46 on average for the two classes. SSD outperformed Faster-RCNN in this case maybe due to the availability of pre-trained model using xView data. 65 | 66 | 67 | 68 | # Reference: 69 | 70 | - Object Detection Baselines in Overhead Imagery with DIUx xView: https://medium.com/@dariusl/object-detection-baselines-in-overhead-imagery-with-diux-xview-c39b1852f24f 71 | 72 | 73 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/__init__.py -------------------------------------------------------------------------------- /create_geojson_basedon_foler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | 9 | ''' 10 | Create a geojson file which contains labels for bounding boxes 11 | according to the files in a given folder 12 | ''' 13 | 14 | import argparse 15 | import os 16 | import geopandas as gpd 17 | import shapely.geometry 18 | import shutil 19 | 20 | 21 | 22 | 23 | 24 | def get_filenames(abs_dirname): 25 | 26 | files = [os.path.join(abs_dirname, f) for f in os.listdir(abs_dirname)] 27 | 28 | i = 0 29 | # parent_folder = os.path.abspath(abs_dirname + "/../") 30 | # seperate_subdir = None 31 | #subdir_name = os.path.join(parent_folder, 'train_small') 32 | #seperate_subdir = subdir_name 33 | #os.mkdir(subdir_name) 34 | name_list = [] 35 | for f in files: 36 | filename_origin = str(f) 37 | filename = filename_origin.split('/')[-1] 38 | #print filename 39 | name_list.append(filename) 40 | # create new subdir if necessary 41 | # if i < N: 42 | #subdir_name = os.path.join(abs_dirname, '{0:03d}'.format(i / N + 1)) 43 | # os.mkdir(subdir_name) 44 | # seperate_subdir = subdir_name 45 | 46 | # copy file to current dir 47 | #f_base = os.path.basename(f) 48 | #shutil.copy(f, os.path.join(subdir_name, f_base)) 49 | i += 1 50 | return name_list 51 | 52 | def geojson_split(geojson_ori, src_dir, savename): 53 | name_list = set(get_filenames(os.path.abspath(src_dir))) 54 | gfN = gpd.read_file(geojson_ori) 55 | index_list = [] 56 | df_len = len(gfN) 57 | 58 | for i in range(0, df_len): 59 | print('idx', i) 60 | series_tmp = gfN.loc[i] 61 | if series_tmp['IMAGE_ID'] in name_list: 62 | index_list.append(i) 63 | geometries = [xy for xy in list(gfN.iloc[index_list]['geometry'])] 64 | crs = {'init': 'epsg:4326'} 65 | gf = gpd.GeoDataFrame(gfN.iloc[index_list], crs=crs, geometry=geometries) 66 | 67 | # geometries = [shapely.geometry.Point(xy) for xy in zip(df.lng, df.lat)] 68 | # gf = gpd.GeoDataFrame(gfN.iloc[0],) 69 | parent_folder = os.path.abspath(geojson_ori + "/../") 70 | 71 | # get folder name 72 | f_base = os.path.basename(src_dir) 73 | save_name = f_base + savename+ '.geojson' 74 | print('saving file: ', save_name) 75 | #path = os.path.join(subdir_name, f_base) 76 | gf.to_file(parent_folder+'/'+ save_name, driver='GeoJSON') 77 | 78 | 79 | 80 | 81 | def parse_args(): 82 | """Parse command line arguments passed to script invocation.""" 83 | parser = argparse.ArgumentParser( 84 | description='Split files into multiple subfolders.') 85 | 86 | parser.add_argument('given_dir', help='directory containing 2048 * 2048 image files') 87 | parser.add_argument('src_geojson', help='source geojson') 88 | 89 | 90 | return parser.parse_args() 91 | 92 | 93 | def main(): 94 | """Module's main entry point (zopectl.command).""" 95 | args = parse_args() 96 | given_dir= args.given_dir 97 | geojson_ori = args.src_geojson 98 | ''' 99 | if not os.path.exists(src_dir): 100 | raise Exception('Directory does not exist ({0}).'.format(src_dir)) 101 | ''' 102 | # the name the geojson will be 103 | savename = '_noblack' 104 | #get_filenames(os.path.abspath(src_dir)) 105 | geojson_split(os.path.abspath(geojson_ori),os.path.abspath(given_dir), savename) 106 | #move_files(os.path.abspath(src_dir)) 107 | #seperate_nfiles(os.path.abspath(src_dir)) 108 | 109 | if __name__ == '__main__': 110 | main() 111 | -------------------------------------------------------------------------------- /create_toy_valdata.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | ''' 9 | From training data, create toy validation data for visualization 10 | of model inferences. Note that the toy validation data may contain 11 | training data as well as data used for validation by the model 12 | 13 | create one folder: harvey_vis_result_toydata 14 | ''' 15 | 16 | import argparse 17 | import os 18 | import shutil 19 | import numpy as np 20 | import itertools 21 | from random import shuffle 22 | ''' 23 | args: 24 | abs_dirname: asolute path to all TRAINING chips, i.e., harvey_train_second 25 | toydir: directory name for toy data 26 | split_percent: 0.2 for toy data 27 | ''' 28 | def seperate_nfiles(abs_dirname, toydir, split_percent): 29 | 30 | files = [os.path.join(abs_dirname, f) for f in os.listdir(abs_dirname)] 31 | 32 | i = 0 33 | parent_folder = os.path.abspath(abs_dirname + "/../") 34 | #seperate_subdir = None 35 | toydir_name = os.path.join(parent_folder, toydir) 36 | os.mkdir(toydir_name) 37 | 38 | # shuffle files 39 | #file_count = len(files) 40 | #perm = np.random.permutation(file_count) 41 | shuffle(files) 42 | split_ind = int(split_percent * len(files)) 43 | num_toy = 0 44 | 45 | for idx, f in enumerate(files): 46 | # create new subdir if necessary 47 | print('idx', idx) 48 | if idx < split_ind: 49 | #subdir_name = os.path.join(abs_dirname, '{0:03d}'.format(i / N + 1)) 50 | # os.mkdir(subdir_name) 51 | # seperate_subdir = subdir_name 52 | 53 | # copy file to current dir 54 | f_base = os.path.basename(f) 55 | shutil.copy(f, os.path.join(toydir_name, f_base)) 56 | num_toy += 1 57 | print('created toy images: ', num_toy) 58 | 59 | 60 | def parse_args(): 61 | """Parse command line arguments passed to script invocation.""" 62 | parser = argparse.ArgumentParser( 63 | description='Split files into multiple subfolders.') 64 | # src dir is for harvey TRAINING DIR 65 | parser.add_argument('src_dir', help='source directory') 66 | 67 | return parser.parse_args() 68 | 69 | 70 | def main(): 71 | """Module's main entry point (zopectl.command).""" 72 | args = parse_args() 73 | src_dir = args.src_dir 74 | 75 | if not os.path.exists(src_dir): 76 | raise Exception('Directory does not exist ({0}).'.format(src_dir)) 77 | 78 | #move_files(os.path.abspath(src_dir)) 79 | toy_dir = 'harvey_vis_result_toydata' 80 | 81 | seperate_nfiles(os.path.abspath(src_dir),toy_dir, 0.2) 82 | 83 | 84 | if __name__ == '__main__': 85 | main() 86 | 87 | -------------------------------------------------------------------------------- /delete_bad_labels.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | ''' 9 | This is to delete bad bboxes from geojson that manually identified 10 | 1. Use the original geojson and training/test chips, run process_wv.py to produce 11 | 512 x 512 small chips with bboxes in a folder called 'harvey_img_inspect_train (test)' 12 | 2. manually inspect every small chips to identify bad bboxes. move bad chips to a folder 13 | 3. get a list of 2048 x 2048 chip names that contain bad examples, plot bboxes upon them 14 | along with bbox uid 15 | 4. manually create a csv, record uids to be deleted. 16 | 5. remove uids from geojson and create a new geojson 17 | ''' 18 | 19 | 20 | ''' 21 | identify_bad_labels.py does 22 | 23 | 1. from a folder of bad labels in small tifs, find out big tiff (2048) ids 24 | plot bbox over these big tiff with uid 25 | 26 | 27 | delete_bad_labels.py does: 28 | 2. after manual inspection. Take a list of bad uid, delete them from geojson and form 29 | a new geojson 30 | ''' 31 | 32 | import aug_util as aug 33 | import wv_util as wv 34 | import matplotlib.pyplot as plt 35 | import numpy as np 36 | import csv 37 | #import matplotlib, copy, skimage, os, tifffile 38 | from skimage import io, morphology, draw 39 | import gdal 40 | from PIL import Image 41 | import random 42 | import json 43 | from tqdm import tqdm 44 | import io 45 | import glob 46 | import shutil 47 | import os 48 | import geopandas as gpd 49 | from PIL import Image, ImageFont, ImageDraw, ImageEnhance 50 | import pandas as pd 51 | 52 | # modified to buffer the bounding boxes by 15 pixels 53 | # return uids of bboxes as well 54 | def get_labels_w_uid(fname): 55 | """ 56 | Gets label data from a geojson label file 57 | Args: 58 | fname: file path to an xView geojson label file 59 | Output: 60 | Returns three arrays: coords, chips, and classes corresponding to the 61 | coordinates, file-names, and classes for each ground truth. 62 | """ 63 | # debug 64 | x_off = 15 65 | y_off = 15 66 | right_shift = 5 # how much shift to the right 67 | add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 68 | with open(fname) as f: 69 | data = json.load(f) 70 | 71 | coords = np.zeros((len(data['features']),4)) 72 | chips = np.zeros((len(data['features'])),dtype="object") 73 | classes = np.zeros((len(data['features']))) 74 | # debug 75 | uids = np.zeros((len(data['features']))) 76 | 77 | for i in tqdm(range(len(data['features']))): 78 | if data['features'][i]['properties']['bb'] != []: 79 | try: 80 | b_id = data['features'][i]['properties']['IMAGE_ID'] 81 | # if b_id == '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif': 82 | # print('found chip!') 83 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 84 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 85 | 86 | ymin = val[3] 87 | ymax = val[1] 88 | val[1] = ymin 89 | val[3] = ymax 90 | chips[i] = b_id 91 | classes[i] = data['features'][i]['properties']['TYPE_ID'] 92 | # debug 93 | uids[i] = int(data['features'][i]['properties']['uniqueid']) 94 | except: 95 | # print('i:', i) 96 | # print(data['features'][i]['properties']['bb']) 97 | pass 98 | if val.shape[0] != 4: 99 | print("Issues at %d!" % i) 100 | else: 101 | coords[i] = val 102 | else: 103 | chips[i] = 'None' 104 | # debug 105 | # added offsets to each coordinates 106 | # need to check the validity of bbox maybe 107 | coords = np.add(coords, add_np) 108 | 109 | return coords, chips, classes, uids 110 | 111 | 112 | 113 | 114 | # draw bboxes with bbox uid 115 | 116 | def draw_bboxes_withindex(img,boxes, uids): 117 | """ 118 | A helper function to draw bounding box rectangles on images 119 | Args: 120 | img: image to be drawn on in array format 121 | boxes: An (N,4) array of bounding boxes 122 | Output: 123 | Image with drawn bounding boxes 124 | """ 125 | source = Image.fromarray(img) 126 | draw = ImageDraw.Draw(source) 127 | w2,h2 = (img.shape[0],img.shape[1]) 128 | 129 | font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSerif.ttf', 40) 130 | #font = ImageFont.truetype('arial.ttf', 24) 131 | 132 | 133 | idx = 0 134 | 135 | for b in boxes: 136 | xmin,ymin,xmax,ymax = b 137 | 138 | for j in range(3): 139 | draw.rectangle(((xmin+j, ymin+j), (xmax+j, ymax+j)), outline="red") 140 | draw.text((xmin+20, ymin+70), str(uids[idx]), font = font) 141 | idx +=1 142 | return source 143 | 144 | 145 | 146 | 147 | # delete bboxes based on bbox uid 148 | # return new geojson 149 | # fname = '../just_buildings_w_uid.geojson' 150 | def delete_bbox_from_geojson(old_geojson, rows_to_delete): 151 | gfN = gpd.read_file(old_geojson) 152 | index_list = [] 153 | df_len = len(gfN) 154 | rows_to_delete = [str(item).split('.')[0] for item in rows_to_delete] 155 | rows_to_delete = list(map(int, rows_to_delete)) 156 | 157 | for i in range(0, df_len): 158 | #print('idx', i) 159 | series_tmp = gfN.loc[i] 160 | if series_tmp['uniqueid'] in set(rows_to_delete): 161 | print('deleting id %s'%str(i)) 162 | continue 163 | index_list.append(i) 164 | geometries = [xy for xy in list(gfN.iloc[index_list]['geometry'])] 165 | crs = {'init': 'epsg:4326'} 166 | gf = gpd.GeoDataFrame(gfN.iloc[index_list], crs=crs, geometry=geometries) 167 | 168 | # geometries = [shapely.geometry.Point(xy) for xy in zip(df.lng, df.lat)] 169 | # gf = gpd.GeoDataFrame(gfN.iloc[0],) 170 | parent_folder = os.path.abspath(old_geojson + "/../") 171 | 172 | # get training or test dir name 173 | f_base = os.path.basename(old_geojson) 174 | 175 | save_name = ''.join(f_base.split('.')[0:-1]) +'_cleaned.geojson' 176 | print(save_name) 177 | gf.to_file(parent_folder+'/'+ save_name, driver='GeoJSON') 178 | 179 | 180 | # take a list of big tifs containing bad labels, draw bboxes 181 | # with uid, and save as png 182 | # args: 183 | # big_chip_list : list of individual big tif file names, 184 | # chip_path : path to all big chip files 185 | 186 | def draw_bbox_on_tiff(bad_big_tif_list, chip_path, coords, chips, classes, uids, save_path): 187 | 188 | 189 | i = 0 190 | # f is the filename without path, chip_name is the filename + path 191 | for f in bad_big_tif_list: 192 | if len(f) < 5: 193 | print('invalid file name: ', f) 194 | continue 195 | chip_name = os.path.join(chip_path, f) 196 | print(chip_name) 197 | 198 | arr = wv.get_image(chip_name) 199 | coords_chip = coords[chips==f] 200 | if coords_chip.shape[0] == 0: 201 | print('no bounding boxes for this image') 202 | continue 203 | classes_chip = classes[chips==f].astype(np.int64) 204 | uids_chip = uids[chips == f].astype(np.int64) 205 | labelled = draw_bboxes_withindex(arr,coords_chip[classes_chip ==1], uids_chip) 206 | print(chip_name) 207 | # plt.figure(figsize=(15,15)) 208 | # plt.axis('off') 209 | # plt.imshow(labelled) 210 | subdir_name = save_path 211 | if os.path.isdir(subdir_name): 212 | save_name = subdir_name +'/' + f + '.png' 213 | print(save_name) 214 | labelled.save(save_name) 215 | else: 216 | os.mkdir(subdir_name) 217 | save_name = subdir_name +'/' + f + '.png' 218 | print(save_name) 219 | labelled.save(save_name) 220 | 221 | 222 | # read all files in a folder which contains small tifs that have 223 | # bad labels. Get all big tiff names in a list 224 | # args: small_tif_dir: absolute path to a dir containing bad small tifs 225 | # return: a list (set) of big tiff names / paths 226 | # i.e. img_20170829_1040010032211E00_2110222_jpeg_compressed_09_04.tif_8.png 227 | def parse_tif_names(small_tif_dir): 228 | #files = [os.path.join(small_tif_dir, f) for f in os.listdir(small_tif_dir)] 229 | fnames = [f for f in os.listdir(small_tif_dir)] 230 | bad_big_tif_list = set() 231 | for fname in fnames: 232 | big_tif_name = fname.split('.')[0] 233 | big_tif_name = '_'.join(big_tif_name.split('_')[1:]) 234 | big_tif_name = big_tif_name + '.tif' 235 | bad_big_tif_list.add(big_tif_name) 236 | return bad_big_tif_list 237 | 238 | 239 | # args: a csv file containing small image names which contains bad labels 240 | # read and parse the big tif names 241 | def parse_tif_names_from_csv(path_to_csv): 242 | #files = [os.path.join(small_tif_dir, f) for f in os.listdir(small_tif_dir)] 243 | #fnames = [f for f in os.listdir(small_tif_dir)] 244 | bad_small_tifs = pd.read_csv(path_to_csv, header = None) 245 | #bad_small_list = set(bad_small_tifs['bad_label'].tolist()) 246 | bad_small_list = set(bad_small_tifs[0].tolist()) 247 | 248 | bad_big_tif_list = set() 249 | for fname in bad_small_list: 250 | big_tif_name = fname.split('.')[0] 251 | big_tif_name = '_'.join(big_tif_name.split('_')[1:]) 252 | big_tif_name = big_tif_name + '.tif' 253 | bad_big_tif_list.add(big_tif_name) 254 | return bad_big_tif_list 255 | 256 | 257 | 258 | 259 | def main(): 260 | # RUN THIS CHUNK TO INSPECT BIG TIFFs WITH BBOXES 261 | # read tif 262 | #small_tif_dir = '/home/ubuntu/anyan/harvey_data/bad_labels_small' 263 | 264 | geojson_file = '../harvey_ms_noclean_2class_fixedprecision.geojson' 265 | #geojson_file = '../added_non_damaged.geojson' 266 | 267 | ''' 268 | bad_small_label_path = '../second_inspection_small_tiff.csv' 269 | coords, chips, classes, uids = get_labels_w_uid(geojson_file) 270 | bad_big_tif_list = parse_tif_names_from_csv(bad_small_label_path) 271 | print('len of bad big tifs: ', len(bad_big_tif_list)) 272 | #print(bad_big_tif_list) 273 | # draw bbox with uid on big tiffs, save as png 274 | save_path = '/home/ubuntu/anyan/harvey_data/bad_labels_big_second' 275 | chip_path = '/home/ubuntu/anyan/harvey_data/filtered_converted_image_buildings' 276 | draw_bbox_on_tiff(bad_big_tif_list, chip_path, coords, chips, classes, uids, save_path) 277 | print('len of bad big tifs: ', len(bad_big_tif_list)) 278 | ''' 279 | # RUN THIS TO DELETE UIDS FROM GEOJSON 280 | # read bad labels from file 281 | bad_label_path = './all_bad_labels_harvey.csv' 282 | bad_label_df = pd.read_csv(bad_label_path, header = None) 283 | bad_label_list = set(bad_label_df[0].tolist()) 284 | 285 | 286 | 287 | # delete from geoson 288 | delete_bbox_from_geojson(geojson_file, bad_label_list) 289 | print('len of bad lables: ', len(bad_label_list)) 290 | 291 | 292 | 293 | if __name__ == "__main__": 294 | main() 295 | -------------------------------------------------------------------------------- /geotiff_to_tiff.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | ''' 9 | convert geotiff to tiff 10 | ''' 11 | 12 | import numpy as np 13 | import csv 14 | #import matplotlib, copy, skimage, os, tifffile 15 | from skimage import io, draw 16 | import gdal 17 | import os 18 | from PIL import Image 19 | import os.path 20 | 21 | 22 | 23 | # take absolute path of geotiff dir 24 | def convert_geotiff_tiff(geotiff_dir, tiff_dir): 25 | files = [os.path.join(geotiff_dir, f) for f in os.listdir(geotiff_dir)] 26 | for f in files: 27 | if not f.endswith('tif'): 28 | continue 29 | tilename = f.split('/')[-1] 30 | # debug 31 | print('tilemame: ', tilename) 32 | ''' 33 | # check file exits 34 | temp_f = os.path.join(tiff_dir, f) 35 | if os.path.isfile(temp_f): 36 | print('file exits, skip') 37 | continue 38 | 39 | ''' 40 | 41 | 42 | ds = gdal.Open(f) 43 | arr1 = ds.GetRasterBand(1).ReadAsArray() 44 | arr2 = ds.GetRasterBand(2).ReadAsArray() 45 | arr3 = ds.GetRasterBand(3).ReadAsArray() 46 | stacked = np.stack((arr1, arr2, arr3), axis = 2) 47 | # create new tiff image 48 | im = Image.fromarray(stacked) 49 | new_tiff = os.path.join(tiff_dir, tilename) 50 | # debug 51 | print('saving image: ', tilename) 52 | im.save(new_tiff) 53 | print('number of files: ', len(files)) 54 | 55 | 56 | def main(): 57 | #geotiff_dir = '../image_tiles_aws' 58 | geotiff_dir = '/home/ubuntu/anyan/harvey_data/NOAA-FEMA/all_files/study_area' 59 | 60 | #tiff_dir = '../converted_image_tiles_aws' 61 | tiff_dir = '../noaa_converted_tiles' 62 | convert_geotiff_tiff(os.path.abspath(geotiff_dir), os.path.abspath(tiff_dir)) 63 | 64 | 65 | if __name__ == '__main__': 66 | main() 67 | -------------------------------------------------------------------------------- /get_data_stat.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | 9 | ''' 10 | Get the statistics of training data and testing data 11 | 1) # of bounding boxes in train / test /total 12 | 2) # of 2048 x 2048 chips in train/ test /total 13 | 3) # of 512 x 512 chips in train / test / total 14 | 4) # of examples each class 15 | ''' 16 | 17 | import argparse 18 | import os 19 | import aug_util as aug 20 | import wv_util as wv 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | import csv 24 | #import matplotlib, copy, skimage, os, tifffile 25 | from skimage import io, morphology, draw 26 | import gdal 27 | from PIL import Image 28 | import random 29 | import json 30 | from tqdm import tqdm 31 | import io 32 | import glob 33 | import shutil 34 | import os 35 | import geopandas as gpd 36 | from PIL import Image, ImageFont, ImageDraw, ImageEnhance 37 | import pandas as pd 38 | 39 | # get bbox count for geojson with ONLY DAMAGED buildings 40 | # get unique chips count 41 | def get_bbox_count(fname): 42 | with open(fname) as f: 43 | data = json.load(f) 44 | 45 | coords = np.zeros((len(data['features']),4)) 46 | chips = np.zeros((len(data['features'])),dtype="object") 47 | classes = np.zeros((len(data['features']))) 48 | # debug 49 | uids = np.zeros((len(data['features']))) 50 | 51 | for i in tqdm(range(len(data['features']))): 52 | if data['features'][i]['properties']['bb'] != []: 53 | try: 54 | b_id = data['features'][i]['properties']['IMAGE_ID'] 55 | # if b_id == '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif': 56 | # print('found chip!') 57 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 58 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 59 | 60 | ymin = val[3] 61 | ymax = val[1] 62 | val[1] = ymin 63 | val[3] = ymax 64 | chips[i] = b_id 65 | classes[i] = data['features'][i]['properties']['TYPE_ID'] 66 | # debug 67 | uids[i] = int(data['features'][i]['properties']['bb_uid']) 68 | except: 69 | # print('i:', i) 70 | # print(data['features'][i]['properties']['bb']) 71 | pass 72 | if val.shape[0] != 4: 73 | print("Issues at %d!" % i) 74 | else: 75 | coords[i] = val 76 | else: 77 | chips[i] = 'None' 78 | # debug 79 | # added offsets to each coordinates 80 | # need to check the validity of bbox maybe 81 | # debug 82 | # mute the shifting of bbox for now 83 | # because no need of adjusting bbox in statistics 84 | #coords = np.add(coords, add_np) 85 | # get the count of unique chips 86 | chip_unique = len(np.unique(chips)) 87 | print('The total number of bboxes for training + test: ', len(data['features'])) 88 | print('The total number of 2048 chips for training + test: ', chip_unique) 89 | 90 | 91 | return coords, chips, classes, uids 92 | 93 | 94 | # for tomnod + MS data, 2 classes 95 | def get_bbox_count_multiclass(fname): 96 | """ 97 | Gets label data from a geojson label file 98 | Args: 99 | fname: file path to an xView geojson label file 100 | Output: 101 | Returns three arrays: coords, chips, and classes corresponding to the 102 | coordinates, file-names, and classes for each ground truth. 103 | """ 104 | # debug 105 | x_off = 15 106 | y_off = 15 107 | right_shift = 5 # how much shift to the right 108 | add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 109 | with open(fname) as f: 110 | data = json.load(f) 111 | 112 | coords = np.zeros((len(data['features']),4)) 113 | chips = np.zeros((len(data['features'])),dtype="object") 114 | classes = np.zeros((len(data['features']))) 115 | # debug 116 | uids = np.zeros((len(data['features']))) 117 | 118 | for i in tqdm(range(len(data['features']))): 119 | if data['features'][i]['properties']['bb'] != []: 120 | try: 121 | b_id = data['features'][i]['properties']['Joined lay'] 122 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 123 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 124 | 125 | chips[i] = b_id 126 | classes[i] = data['features'][i]['properties']['type'] 127 | # debug 128 | uids[i] = int(data['features'][i]['properties']['uniqueid']) 129 | except: 130 | pass 131 | if val.shape[0] != 4: 132 | print("Issues at %d!" % i) 133 | else: 134 | coords[i] = val 135 | else: 136 | chips[i] = 'None' 137 | # debug 138 | # added offsets to each coordinates 139 | # need to check the validity of bbox maybe 140 | chip_unique = len(np.unique(chips)) 141 | print('The total number of bboxes for training + test: ', len(data['features'])) 142 | print('The total number of bboxes for damaged buildings training + test: ', classes[classes == 1].shape[0]) 143 | print('The total number of bboxes for non-damaged buildings training + test: ', classes[classes == 2].shape[0]) 144 | 145 | print('The total number of 2048 chips for training + test: ', chip_unique) 146 | return coords, chips, classes, uids 147 | 148 | 149 | 150 | 151 | 152 | 153 | def parse_args(): 154 | """Parse command line arguments passed to script invocation.""" 155 | parser = argparse.ArgumentParser( 156 | description='Get statistics for training data and test data from geojson and tif images.') 157 | 158 | parser.add_argument('src_geojson', help='source geojson') 159 | 160 | return parser.parse_args() 161 | 162 | 163 | def main(): 164 | args = parse_args() 165 | geojson_path = args.src_geojson 166 | #geojson_path = '../just_buildings_w_uid_second_round.geojson' 167 | get_bbox_count_multiclass(geojson_path) 168 | 169 | 170 | 171 | if __name__ == "__main__": 172 | main() 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /identify_bad_labels.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | 9 | ''' 10 | This is to delete bad bboxes from geojson that manually identified 11 | 1. Use the original geojson and training/test chips, run process_wv.py to produce 12 | 512 x 512 small chips with bboxes in a folder called 'harvey_img_inspect_train (test)' 13 | 2. manually inspect every small chips to identify bad bboxes. move bad chips to a folder 14 | 3. get a list of 2048 x 2048 chip names that contain bad examples, plot bboxes upon them 15 | along with bbox uid 16 | 4. manually create a csv, record uids to be deleted. 17 | 5. remove uids from geojson and create a new geojson 18 | ''' 19 | 20 | 21 | ''' 22 | This script does: 23 | 24 | 1. from a folder of bad labels in small tifs, find out big tiff (2048) ids 25 | plot bbox over these big tiff with uid 26 | 27 | 28 | delete_bad_labels.py does: 29 | 2. after manual inspection. Take a list of bad uid, delete them from geojson and form 30 | a new geojson 31 | ''' 32 | 33 | import aug_util as aug 34 | import wv_util as wv 35 | import matplotlib.pyplot as plt 36 | import numpy as np 37 | import csv 38 | #import matplotlib, copy, skimage, os, tifffile 39 | from skimage import io, morphology, draw 40 | import gdal 41 | from PIL import Image 42 | import random 43 | import json 44 | from tqdm import tqdm 45 | import io 46 | import glob 47 | import shutil 48 | import os 49 | import geopandas as gpd 50 | from PIL import Image, ImageFont, ImageDraw, ImageEnhance 51 | 52 | 53 | # modified to buffer the bounding boxes by 15 pixels 54 | # return uids of bboxes as well 55 | def get_labels_w_uid(fname): 56 | """ 57 | Gets label data from a geojson label file 58 | Args: 59 | fname: file path to an xView geojson label file 60 | Output: 61 | Returns three arrays: coords, chips, and classes corresponding to the 62 | coordinates, file-names, and classes for each ground truth. 63 | """ 64 | # debug 65 | x_off = 15 66 | y_off = 15 67 | right_shift = 5 # how much shift to the right 68 | add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 69 | with open(fname) as f: 70 | data = json.load(f) 71 | 72 | coords = np.zeros((len(data['features']),4)) 73 | chips = np.zeros((len(data['features'])),dtype="object") 74 | classes = np.zeros((len(data['features']))) 75 | # debug 76 | uids = np.zeros((len(data['features']))) 77 | 78 | for i in tqdm(range(len(data['features']))): 79 | if data['features'][i]['properties']['bb'] != []: 80 | try: 81 | b_id = data['features'][i]['properties']['IMAGE_ID'] 82 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 83 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 84 | 85 | ymin = val[3] 86 | ymax = val[1] 87 | val[1] = ymin 88 | val[3] = ymax 89 | chips[i] = b_id 90 | classes[i] = data['features'][i]['properties']['TYPE_ID'] 91 | # debug 92 | uids[i] = int(data['features'][i]['properties']['bb_uid']) 93 | except: 94 | # print('i:', i) 95 | # print(data['features'][i]['properties']['bb']) 96 | pass 97 | if val.shape[0] != 4: 98 | print("Issues at %d!" % i) 99 | else: 100 | coords[i] = val 101 | else: 102 | chips[i] = 'None' 103 | # debug 104 | coords = np.add(coords, add_np) 105 | 106 | return coords, chips, classes, uids 107 | 108 | 109 | 110 | 111 | # draw bboxes with bbox uid 112 | 113 | def draw_bboxes_withindex(img,boxes, uids): 114 | """ 115 | A helper function to draw bounding box rectangles on images 116 | Args: 117 | img: image to be drawn on in array format 118 | boxes: An (N,4) array of bounding boxes 119 | Output: 120 | Image with drawn bounding boxes 121 | """ 122 | source = Image.fromarray(img) 123 | draw = ImageDraw.Draw(source) 124 | w2,h2 = (img.shape[0],img.shape[1]) 125 | 126 | font = ImageFont.truetype('/usr/share/fonts/truetype/freefont/FreeSerif.ttf', 40) 127 | #font = ImageFont.truetype('arial.ttf', 24) 128 | 129 | 130 | idx = 0 131 | 132 | for b in boxes: 133 | xmin,ymin,xmax,ymax = b 134 | 135 | for j in range(3): 136 | draw.rectangle(((xmin+j, ymin+j), (xmax+j, ymax+j)), outline="red") 137 | draw.text((xmin+20, ymin+70), str(uids[idx]), font = font) 138 | idx +=1 139 | return source 140 | 141 | 142 | 143 | 144 | # delete bboxes based on bbox uid 145 | # return new geojson 146 | # fname = '../just_buildings_w_uid.geojson' 147 | def delete_bbox_from_geojson(old_geojson, rows_to_delete): 148 | gfN = gpd.read_file(old_geojson) 149 | index_list = [] 150 | df_len = len(gfN) 151 | 152 | 153 | for i in range(0, df_len): 154 | print('idx', i) 155 | series_tmp = gfN.loc[i] 156 | if series_tmp['bb_uid'] in set(rows_to_delete): 157 | continue 158 | index_list.append(i) 159 | geometries = [xy for xy in list(gfN.iloc[index_list]['geometry'])] 160 | crs = {'init': 'epsg:4326'} 161 | gf = gpd.GeoDataFrame(gfN.iloc[index_list], crs=crs, geometry=geometries) 162 | 163 | # geometries = [shapely.geometry.Point(xy) for xy in zip(df.lng, df.lat)] 164 | # gf = gpd.GeoDataFrame(gfN.iloc[0],) 165 | parent_folder = os.path.abspath(old_geojson + "/../") 166 | 167 | # get training or test dir name 168 | f_base = os.path.basename(old_geojson) 169 | 170 | save_name = ''.join(f_base.split('.')[0:-1]) +'_cleaned.geojson' 171 | print(save_name) 172 | gf.to_file(parent_folder+'/'+ save_name, driver='GeoJSON') 173 | 174 | 175 | # take a list of big tifs containing bad labels, draw bboxes 176 | # with uid, and save as png 177 | # args: 178 | # big_chip_list : list of individual big tif file names, 179 | # chip_path : path to all big chip files 180 | 181 | def draw_bbox_on_tiff(bad_big_tif_list, chip_path, coords, chips, classes, uids, save_path): 182 | 183 | 184 | i = 0 185 | # f is the filename without path, chip_name is the filename + path 186 | for f in bad_big_tif_list: 187 | if len(f) < 5: 188 | print('invalid file name: ', f) 189 | continue 190 | chip_name = os.path.join(chip_path, f) 191 | print(chip_name) 192 | 193 | arr = wv.get_image(chip_name) 194 | coords_chip = coords[chips==f] 195 | if coords_chip.shape[0] == 0: 196 | print('no bounding boxes for this image') 197 | continue 198 | classes_chip = classes[chips==f].astype(np.int64) 199 | uids_chip = uids[chips == f].astype(np.int64) 200 | labelled = draw_bboxes_withindex(arr,coords_chip[classes_chip ==1], uids_chip) 201 | print(chip_name) 202 | # plt.figure(figsize=(15,15)) 203 | # plt.axis('off') 204 | # plt.imshow(labelled) 205 | subdir_name = save_path 206 | if os.path.isdir(subdir_name): 207 | save_name = subdir_name +'/' + f + '.png' 208 | print(save_name) 209 | labelled.save(save_name) 210 | else: 211 | os.mkdir(subdir_name) 212 | save_name = subdir_name +'/' + f + '.png' 213 | print(save_name) 214 | labelled.save(save_name) 215 | 216 | 217 | # read all files in a folder which contains small tifs that have 218 | # bad labels. Get all big tiff names in a list 219 | # args: small_tif_dir: absolute path to a dir containing bad small tifs 220 | # return: a list (set) of big tiff names / paths 221 | # i.e. img_20170829_1040010032211E00_2110222_jpeg_compressed_09_04.tif_8.png 222 | def parse_tif_names(small_tif_dir): 223 | #files = [os.path.join(small_tif_dir, f) for f in os.listdir(small_tif_dir)] 224 | fnames = [f for f in os.listdir(small_tif_dir)] 225 | bad_big_tif_list = set() 226 | for fname in fnames: 227 | big_tif_name = fname.split('.')[0] 228 | big_tif_name = '_'.join(big_tif_name.split('_')[1:]) 229 | big_tif_name = big_tif_name + '.tif' 230 | bad_big_tif_list.add(big_tif_name) 231 | return bad_big_tif_list 232 | 233 | 234 | 235 | 236 | def main(): 237 | # RUN THIS CHUNK TO INSPECT BIG TIFFs WITH BBOXES 238 | # read tif 239 | small_tif_dir = '/home/ubuntu/anyan/harvey_data/bad_labels_small' 240 | geojson_file = '../just_buildings_w_uid.geojson' 241 | coords, chips, classes, uids = get_labels_w_uid(geojson_file) 242 | 243 | bad_big_tif_list = parse_tif_names(small_tif_dir) 244 | print('len of bad big tifs: ', len(bad_big_tif_list)) 245 | print(bad_big_tif_list) 246 | # draw bbox with uid on big tiffs, save as png 247 | save_path = '/home/ubuntu/anyan/harvey_data/bad_labels_big' 248 | chip_path = '/home/ubuntu/anyan/harvey_data/filtered_converted_image_buildings' 249 | draw_bbox_on_tiff(bad_big_tif_list, chip_path, coords, chips, classes, uids, save_path) 250 | print('len of bad big tifs: ', len(bad_big_tif_list)) 251 | 252 | # RUN THIS TO DELETE UIDS FROM GEOJSON 253 | # read bad labels from file 254 | 255 | 256 | # delete from geoson 257 | 258 | 259 | 260 | if __name__ == "__main__": 261 | main() 262 | -------------------------------------------------------------------------------- /inference/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/inference/.DS_Store -------------------------------------------------------------------------------- /inference/.create_detections.py.swo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/inference/.create_detections.py.swo -------------------------------------------------------------------------------- /inference/.create_detections.py.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/inference/.create_detections.py.swp -------------------------------------------------------------------------------- /inference/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. 202 | -------------------------------------------------------------------------------- /inference/README.md: -------------------------------------------------------------------------------- 1 | ## xView Baselines and Scoring 2 | 3 | ### Code 4 | 5 | This repository contains the pre-trained xView baseline models (see our [blog post](https://medium.com/@dariusl/object-detection-baselines-in-overhead-imagery-with-diux-xview-c39b1852f24f)) as well as code for inference and scoring. Class ID to name mappings are in the 'xview_class_labels.txt' file 6 | 7 | Inference and scoring code are under the 'inference/' and 'scoring/' folders, respectively. Inside 'inference/' we provide a python script 'create_detections.py' for exporting detections from a single xView TIF image, given a frozen model. There is also a script 'create_detections.sh' for exporting detections from multiple xView TIF images. Exported detections are in format 8 | 9 | 10 | |X min|Y min|X max|Y max|Class ID|Confidence| 11 | |---|---|---|---|---|---| 12 | 13 | 14 | which is the proper format for submitting to the xView challenge portal. You can use the given pre-trained baseline models for the '-c' checkpoint parameter. 15 | 16 | The 'scoring/' folder contains code for evaluating a set of predictions (exported from 'create_detections.py' or 'create_detections.sh') given a ground truth label geojson. The script 'score.py' calculates scores: total mean average precision (mAP), per-class mAP, mAP for small/med/large classes, mAP for rare/common classes, F1 score, mean precision, and mean recall. We use the PASCAL VOC method for computing mean average precision and calculate mean precision and mean recall using the formulas: 17 | 18 | Precision = (True Positives) / (True Positives + False Positives) 19 | 20 | Recall = (True Positives) / (True Positives + False Negatives) 21 | 22 | averaged over all classes and files. Class splits are shown at the bottom of this README. 23 | 24 | Pre-trained baseline models can be found in a zip file under the 'releases' tab. There are three models: vanilla, multires, and multires_aug which are described [here](https://medium.com/@dariusl/object-detection-baselines-in-overhead-imagery-with-diux-xview-c39b1852f24f). Inside the zip file are three folders and three pb files. The pb files are frozen models: they can be plugged into the detection scripts right away. Inside each respective folder are the tensorflow checkpoint files that can be used for fine-tuning. 25 | 26 | 27 | ### Class Splits 28 | 29 | Small: 30 | ['Passenger Vehicle', 'Small car', 'Bus', 'Pickup Truck', 'Utility Truck', 'Truck', 'Cargo Truck', 'Truck Tractor', 'Trailer', 'Truck Tractor w/ Flatbed Trailer', 'Crane Truck', 'Motorboat', 'Dump truck', 'Tractor', 'Front loader/Bulldozer', 'Excavator', 'Cement mixer', 'Ground grader', 'Shipping container'] 31 | 32 | Medium: 33 | ['Fixed-wing aircraft', 'Small aircraft', 'Helicopter', 'Truck Tractor w/ Box Trailer', 'Truck Tractor w/ Liquid Tank', 'Railway vehicle', 'Passenger car', 'Cargo/container car', 'Flat car', 'Tank car', 'Locomotive', 'Sailboat', 'Tugboat', 'Fishing vessel', 'Yacht', 'Engineering vehicle', 'Reach stacker', 'Mobile crane', 'Haul truck', 'Hut/Tent', 'Shed', 'Building', 'Damaged/demolished building', 'Helipad', 'Storage Tank', 'Pylon', 'Tower'] 34 | 35 | 36 | Large: 37 | ['Passenger/cargo plane', 'Maritime vessel', 'Barge', 'Ferry', 'Container ship', 'Oil Tanker', 'Tower crane', 'Container crane', 'Straddle carrier', 'Aircraft Hangar', 'Facility', 'Construction site', 'Vehicle Lot', 'Shipping container lot'] 38 | 39 | --- 40 | 41 | Common: 42 | ['Passenger/cargo plane', 'Passenger Vehicle', 'Small car', 'Bus', 'Pickup Truck', 'Utility Truck', 'Truck', 'Cargo Truck', 'Truck Tractor w/ Box Trailer', 'Truck Tractor', 'Trailer', 'Truck Tractor w/ Flatbed Trailer', 'Passenger car', 'Cargo/container car', 'Motorboat', 'Fishing vessel', 'Dump truck', 'Front loader/Bulldozer', 'Excavator', 'Hut/Tent', 'Shed', 'Building', 'Damaged/demolished building', 'Facility', 'Construction site', 'Vehicle Lot', 'Storage Tank', 'Shipping container lot', 'Shipping container'] 43 | 44 | Rare: 45 | ['Fixed-wing aircraft', 'Small aircraft', 'Helicopter', 'Truck Tractor w/ Liquid Tank', 'Crane Truck', 'Railway vehicle', 'Flat car', 'Tank car', 'Locomotive', 'Maritime vessel', 'Sailboat', 'Tugboat', 'Barge', 'Ferry', 'Yacht', 'Container ship', 'Oil Tanker', 'Engineering vehicle', 'Tower crane', 'Container crane', 'Reach stacker', 'Straddle carrier', 'Mobile crane', 'Haul truck', 'Tractor', 'Cement mixer', 'Ground grader', 'Aircraft Hangar', 'Helipad', 'Pylon', 'Tower'] 46 | -------------------------------------------------------------------------------- /inference/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/inference/__init__.py -------------------------------------------------------------------------------- /inference/__pycache__/det_util.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/inference/__pycache__/det_util.cpython-36.pyc -------------------------------------------------------------------------------- /inference/create_detections.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Defense Innovation Unit Experimental 3 | All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Modifications copyright (C) 2018 18 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 19 | Written by An Yan 20 | 21 | """ 22 | 23 | 24 | import numpy as np 25 | import tensorflow as tf 26 | from PIL import Image 27 | from PIL import ImageDraw 28 | from PIL import ImageFont 29 | from tqdm import tqdm 30 | import argparse 31 | from det_util import generate_detections, generate_detection_for_single_image 32 | import json 33 | 34 | #import sys 35 | #from os import path 36 | #sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) ) 37 | #sys.path.append('../') 38 | #from data_utilities.process_wv_ms import detect_blackblock 39 | #from .wv_util import chip_image 40 | """ 41 | Inference script to generate a file of predictions given an input. 42 | 43 | Args: 44 | checkpoint: A filepath to the exported pb (model) file. 45 | ie ("saved_model.pb") 46 | 47 | chip_size: An integer describing how large chips of test image should be 48 | 49 | input: A filepath to a single test chip 50 | ie ("1192.tif") 51 | 52 | output: A filepath where the script will save its predictions 53 | ie ("predictions.txt") 54 | 55 | 56 | Outputs: 57 | Writes a file specified by the 'output' parameter containing predictions for the model. 58 | Per-line format: xmin ymin xmax ymax class_prediction score_prediction 59 | Note that the variable "num_preds" is dependent on the trained model 60 | (default is 250, but other models have differing numbers of predictions) 61 | 62 | """ 63 | 64 | def chip_image(img, chip_size=(300,300)): 65 | """ 66 | Segmzent an image into NxWxH chips 67 | 68 | Args: 69 | img : Array of image to be chipped 70 | chip_size : A list of (width,height) dimensions for chips 71 | 72 | Outputs: 73 | An ndarray of shape (N,W,H,3) where N is the number of chips, 74 | W is the width per chip, and H is the height per chip. 75 | 76 | """ 77 | width,height,_ = img.shape 78 | wn,hn = chip_size 79 | images = np.zeros((int(width/wn) * int(height/hn),wn,hn,3)) 80 | k = 0 81 | for i in tqdm(range(int(width/wn))): 82 | for j in range(int(height/hn)): 83 | 84 | # chip = img[wn*i:wn*(i+1),hn*j:hn*(j+1),:3] 85 | chip = img[hn*j:hn*(j+1), wn*i:wn*(i+1),:3] 86 | images[k]=chip 87 | 88 | k = k + 1 89 | 90 | return images.astype(np.uint8) 91 | 92 | 93 | 94 | 95 | 96 | # # changed this function to discard bboxes that cut off to have less than 20 pixels in w/h 97 | def chip_image_withboxes(img,coords,shape=(300,300)): 98 | """ 99 | Chip an image and get relative coordinates and classes. Bounding boxes that pass into 100 | multiple chips are clipped: each portion that is in a chip is labeled. For example, 101 | half a building will be labeled if it is cut off in a chip. If there are no boxes, 102 | the boxes array will be [[0,0,0,0]] and classes [0]. 103 | Note: This chip_image method is only tested on xView data-- there are some image manipulations that can mess up different images. 104 | 105 | Args: 106 | img: the image to be chipped in array format 107 | coords: an (N,4) array of bounding box coordinates for that image 108 | classes: an (N,1) array of classes for each bounding box 109 | shape: an (W,H) tuple indicating width and height of chips 110 | 111 | Output: 112 | An image array of shape (M,W,H,C), where M is the number of chips, 113 | W and H are the dimensions of the image, and C is the number of color 114 | channels. Also returns boxes and classes dictionaries for each corresponding chip. 115 | """ 116 | height,width,_ = img.shape 117 | wn,hn = shape 118 | 119 | w_num,h_num = (int(width/wn),int(height/hn)) 120 | images = np.zeros((w_num*h_num,hn,wn,3)) 121 | total_boxes = {} 122 | #total_classes = {} 123 | 124 | 125 | # debug 126 | # TODO: determine whether to discard bboxes at the edge 127 | threshold = 30 # threshold of # of pixels to discard bbox 128 | 129 | k = 0 130 | for i in range(w_num): 131 | for j in range(h_num): 132 | x = np.logical_or( np.logical_and((coords[:,0]<((i+1)*wn)),(coords[:,0]>(i*wn))), 133 | np.logical_and((coords[:,2]<((i+1)*wn)),(coords[:,2]>(i*wn)))) 134 | 135 | 136 | out = coords[x] 137 | y = np.logical_or( np.logical_and((out[:,1]<((j+1)*hn)),(out[:,1]>(j*hn))), 138 | np.logical_and((out[:,3]<((j+1)*hn)),(out[:,3]>(j*hn)))) 139 | outn = out[y] 140 | out = np.transpose(np.vstack((np.clip(outn[:,0]-(wn*i),0,wn), 141 | np.clip(outn[:,1]-(hn*j),0,hn), 142 | np.clip(outn[:,2]-(wn*i),0,wn), 143 | np.clip(outn[:,3]-(hn*j),0,hn)))) 144 | # box_classes = classes[x][y] 145 | # debug 146 | # remove bboxes that only have less than 20 pixels in w/h left in the image 147 | # only loop through ones that have 0 or wn/hn in the 4 coordinates 148 | rows_to_delete = list() 149 | for m in range(out.shape[0]): 150 | if(np.any([out[m] == 0]) or np.any([out[m] == wn]) or np.any([out[m] == hn])): 151 | # see whether the width of bbox is less than 10 pixels? 152 | bbox_w = out[m][2] - out[m][0] 153 | bbox_h = out[m][3] - out[m][1] 154 | if bbox_w < threshold or bbox_h < threshold: 155 | rows_to_delete.append(m) 156 | 157 | # discard this bbox 158 | 159 | out = np.delete(out, rows_to_delete, axis=0) 160 | # box_classes = np.delete(box_classes, rows_to_delete, axis=0) 161 | 162 | 163 | if out.shape[0] != 0: 164 | total_boxes[k] = out 165 | # total_classes[k] = box_classes 166 | else: 167 | total_boxes[k] = np.array([[0,0,0,0]]) 168 | # total_classes[k] = np.array([0]) 169 | 170 | chip = img[hn*j:hn*(j+1),wn*i:wn*(i+1),:3] 171 | images[k]=chip 172 | k = k + 1 173 | 174 | return images.astype(np.uint8),total_boxes 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | def draw_bboxes(img,boxes,classes): 185 | """ 186 | Draw bounding boxes on top of an image 187 | 188 | Args: 189 | img : Array of image to be modified 190 | boxes: An (N,4) array of boxes to draw, where N is the number of boxes. 191 | classes: An (N,1) array of classes corresponding to each bounding box. 192 | 193 | Outputs: 194 | An array of the same shape as 'img' with bounding boxes 195 | and classes drawn 196 | 197 | """ 198 | source = Image.fromarray(img) 199 | draw = ImageDraw.Draw(source) 200 | w2,h2 = (img.shape[0],img.shape[1]) 201 | 202 | idx = 0 203 | 204 | for i in range(len(boxes)): 205 | xmin,ymin,xmax,ymax = boxes[i] 206 | c = classes[i] 207 | 208 | draw.text((xmin+15,ymin+15), str(c)) 209 | if c== 1: 210 | for j in range(4): 211 | draw.rectangle(((xmin+j, ymin+j), (xmax+j, ymax+j)), outline="red") 212 | else: 213 | for j in range(4): 214 | draw.rectangle(((xmin+j, ymin+j), (xmax+j, ymax+j)), outline="green") 215 | 216 | return source 217 | 218 | 219 | 220 | # debug 221 | # load building footprints from geojson file 222 | # Detections will only be made in chips that contain any buildings 223 | def load_building_footprint_nondamaged(fname): 224 | """ 225 | Gets label data from a geojson label file 226 | Args: 227 | fname: file path to an xView geojson label file 228 | Output: 229 | Returns three arrays: coords, chips, and classes corresponding to the 230 | coordinates, file-names, and classes for each ground truth. 231 | """ 232 | # debug 233 | x_off = 15 234 | y_off = 15 235 | right_shift = 5 # how much shift to the right 236 | add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 237 | with open(fname) as f: 238 | data = json.load(f) 239 | 240 | coords = np.zeros((len(data['features']),4)) 241 | chips = np.zeros((len(data['features'])),dtype="object") 242 | classes = np.zeros((len(data['features']))) 243 | # debug 244 | uids = np.zeros((len(data['features']))) 245 | 246 | for i in tqdm(range(len(data['features']))): 247 | if data['features'][i]['properties']['bb'] != []: 248 | try: 249 | b_id = data['features'][i]['properties']['Joined lay'] 250 | # if b_id == '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif': 251 | # print('found chip!') 252 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 253 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 254 | 255 | chips[i] = b_id 256 | classes[i] = data['features'][i]['properties']['type'] 257 | # debug 258 | uids[i] = int(data['features'][i]['properties']['uniqueid']) 259 | except: 260 | # print('i:', i) 261 | # print(data['features'][i]['properties']['bb']) 262 | pass 263 | if val.shape[0] != 4: 264 | print("Issues at %d!" % i) 265 | else: 266 | coords[i] = val 267 | else: 268 | chips[i] = 'None' 269 | # debug 270 | # added offsets to each coordinates 271 | # need to check the validity of bbox maybe 272 | coords = np.add(coords, add_np) 273 | 274 | return coords, chips 275 | 276 | 277 | 278 | 279 | 280 | if __name__ == "__main__": 281 | # debug 282 | # added loading geojson to crop out chips with buildings 283 | parser = argparse.ArgumentParser() 284 | parser.add_argument("json_filepath",help="geojson file") 285 | parser.add_argument("-c","--checkpoint", default='pbs/model.pb', help="Path to saved model") 286 | parser.add_argument("-cs", "--chip_size", default=300, type=int, help="Size in pixels to chip input image") 287 | parser.add_argument("input", help="Path to test chip") 288 | parser.add_argument("-o","--output",default="predictions.txt",help="Filepath of desired output") 289 | args = parser.parse_args() 290 | 291 | #Parse and chip images 292 | arr = np.array(Image.open(args.input)) 293 | chip_size = (args.chip_size,args.chip_size) 294 | #images = chip_image(arr,chip_size) 295 | #print(images.shape) 296 | image_name = args.input.split("/")[-1] 297 | num_preds = 100 298 | # TODO: loading building footprints, only make detections with the chips that have bboxes 299 | # write back class = 0, bboxes == [0,0,0,0] to the chips that do not contain building footprints 300 | #coords,chips,classes = wv.get_labels(args.json_filepath) 301 | coords,chips= load_building_footprint_nondamaged(args.json_filepath) 302 | im,box_chip = chip_image_withboxes(arr,coords[chips==image_name],chip_size) 303 | #im = chip_image(arr,chip_size) 304 | 305 | # debug 306 | # TODO: if there are black images in test images. Then need to remove black chips here 307 | # automatic cloud removal if nessessary 308 | 309 | 310 | 311 | boxes = [] 312 | scores = [] 313 | classes = [] 314 | k = 0 315 | 316 | images_list = [] 317 | empty_image_idx = [] 318 | for idx, image in enumerate(im): 319 | # skip chips that do not have buildings, avoid feeding them into inference 320 | if len(box_chip[idx]) == 0 or (len(box_chip[idx]) == 1 and np.all(box_chip[idx]==0)): 321 | empty_image_idx.append(idx) 322 | k = k+1 323 | else: 324 | images_list.append(image) 325 | images = np.array(images_list) 326 | images.astype(np.uint8) 327 | print('number of images without bboxes: ', k) 328 | print('images shape: ', images.shape) 329 | 330 | i = 0 331 | boxes_pred, scores_pred, classes_pred = generate_detections(args.checkpoint,images) 332 | 333 | 334 | 335 | # debug 336 | for idx, image in enumerate(im): 337 | if idx in set(empty_image_idx): 338 | 339 | box = np.zeros((1, num_preds, 4)) 340 | score = np.zeros((1, num_preds)) 341 | clss = np.zeros((1, num_preds)) 342 | boxes.append(box) 343 | scores.append(score) 344 | classes.append(clss) 345 | else: 346 | #continue 347 | box_pred, score_pred, cls_pred = boxes_pred[i], scores_pred[i], classes_pred[i] 348 | # debug 349 | #print('box_pred', box_pred) 350 | #print('cls_pred', cls_pred) 351 | print('shape of box_pred', box_pred.shape) 352 | print('shape of score_pred', score_pred.shape) 353 | boxes.append(box_pred) 354 | scores.append(score_pred) 355 | classes.append(cls_pred) 356 | i = i+1 357 | # debug 358 | #print('boxes', boxes) 359 | print('shape of boxes', len(boxes)) 360 | print('shape of one of the boxes', boxes[0].shape) 361 | 362 | boxes = np.squeeze(np.array(boxes)) 363 | scores = np.squeeze(np.array(scores)) 364 | classes = np.squeeze(np.array(classes)) 365 | 366 | #generate detections 367 | #boxes, scores, classes = generate_detections(args.checkpoint,images) 368 | 369 | #Process boxes to be full-sized 370 | width,height,_ = arr.shape 371 | cwn,chn = (chip_size) 372 | wn,hn = (int(width/cwn),int(height/chn)) 373 | 374 | 375 | # changed to 100 in harvey situtation 376 | #num_preds = 250 377 | # num_preds = 100 378 | 379 | # debug 380 | #bfull = boxes[:wn*hn].reshape((wn,hn,num_preds,4)) # original 381 | bfull = boxes[:wn*hn].reshape((hn, wn,num_preds,4)) 382 | 383 | # debug 384 | # commented out original transform, because the way the images 385 | # are chipped are changed 386 | 387 | b2 = np.zeros(bfull.shape) 388 | b2[:,:,:,0] = bfull[:,:,:,1] 389 | b2[:,:,:,1] = bfull[:,:,:,0] 390 | b2[:,:,:,2] = bfull[:,:,:,3] 391 | b2[:,:,:,3] = bfull[:,:,:,2] 392 | 393 | bfull = b2 394 | 395 | 396 | 397 | bfull[:,:,:,0] *= cwn 398 | bfull[:,:,:,2] *= cwn 399 | bfull[:,:,:,1] *= chn 400 | bfull[:,:,:,3] *= chn 401 | # debug 402 | #for i in range(wn): 403 | # for j in range(hn): 404 | for i in range(hn): 405 | for j in range(wn): 406 | ''' 407 | # original 408 | bfull[i,j,:,0] += j*cwn 409 | bfull[i,j,:,2] += j*cwn 410 | 411 | bfull[i,j,:,1] += i*chn 412 | bfull[i,j,:,3] += i*chn 413 | ''' 414 | 415 | 416 | bfull[i,j,:,0] += i*cwn 417 | bfull[i,j,:,2] += i*cwn 418 | 419 | bfull[i,j,:,1] += j*chn 420 | bfull[i,j,:,3] += j*chn 421 | 422 | bfull = bfull.reshape((hn*wn,num_preds,4)) 423 | 424 | 425 | #only display boxes with confidence > .5 426 | bs = bfull[scores > .5] 427 | cs = classes[scores>.5] 428 | s = args.input.split("/")[::-1] 429 | draw_bboxes(arr,bs,cs).save("p_bboxes/"+s[0].split(".")[0] + ".png") 430 | 431 | 432 | with open(args.output,'w') as f: 433 | for i in range(bfull.shape[0]): 434 | for j in range(bfull[i].shape[0]): 435 | #box should be xmin: ymin xmax ymax 436 | box = bfull[i,j] 437 | class_prediction = classes[i,j] 438 | score_prediction = scores[i,j] 439 | f.write('%d %d %d %d %d %f \n' % \ 440 | (box[0],box[1],box[2],box[3],int(class_prediction),score_prediction)) 441 | -------------------------------------------------------------------------------- /inference/create_detections.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #TESTLIST="20170902_105001000B9D7E00_3030320_jpeg_compressed_08_05.tif 20170831_105001000B95E200_3002321_jpeg_compressed_02_03.tif 20170901_1030010070C13600_3030230_jpeg_compressed_03_09.tif 20170829_1040010032211E00_2110223_jpeg_compressed_03_07.tif 20170829_1040010032211E00_2110200_jpeg_compressed_08_07.tif" 4 | #TESTLIST="20170902_105001000B9D7E00_3030320_jpeg_compressed_08_05.tif" 5 | 6 | DIR='/home/ubuntu/anyan/harvey_data/harvey_test_bigtiff_v3/' 7 | for i in "$DIR"/*; do 8 | 9 | #for i in $TESTLIST; do 10 | #echo $i 11 | echo $(basename "$i") 12 | python create_detections.py /home/ubuntu/anyan/harvey_data/harvey_tomnod_final.geojson -c /home/ubuntu/anyan/models/research/models/tomnod_ssd_inceptionv2_2class_aug/tomnod_ssd_inceptionv2_2class_aug_147481.pb -cs 200 -o 'tomnod_2class_preds_output/'$(basename "$i")'.txt' $i 13 | done 14 | -------------------------------------------------------------------------------- /inference/det_util.py: -------------------------------------------------------------------------------- 1 | # Original work Copyright 2017 The TensorFlow Authors. All Rights Reserved. 2 | # Modifications Copyright 2018 Defense Innovation Unit Experimental. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | 17 | import tensorflow as tf 18 | import numpy as np 19 | from tqdm import tqdm 20 | 21 | def generate_detections(checkpoint,images): 22 | 23 | print("Creating Graph...") 24 | detection_graph = tf.Graph() 25 | with detection_graph.as_default(): 26 | od_graph_def = tf.GraphDef() 27 | with tf.gfile.GFile(checkpoint, 'rb') as fid: 28 | serialized_graph = fid.read() 29 | od_graph_def.ParseFromString(serialized_graph) 30 | tf.import_graph_def(od_graph_def, name='') 31 | 32 | boxes = [] 33 | scores = [] 34 | classes = [] 35 | k = 0 36 | with detection_graph.as_default(): 37 | with tf.Session(graph=detection_graph) as sess: 38 | for image_np in tqdm(images): 39 | image_np_expanded = np.expand_dims(image_np, axis=0) 40 | image_tensor = detection_graph.get_tensor_by_name('image_tensor:0') 41 | box = detection_graph.get_tensor_by_name('detection_boxes:0') 42 | score = detection_graph.get_tensor_by_name('detection_scores:0') 43 | clss = detection_graph.get_tensor_by_name('detection_classes:0') 44 | num_detections = detection_graph.get_tensor_by_name('num_detections:0') 45 | # Actual detection. 46 | (box, score, clss, num_detections) = sess.run( 47 | [box, score, clss, num_detections], 48 | feed_dict={image_tensor: image_np_expanded}) 49 | 50 | boxes.append(box) 51 | scores.append(score) 52 | classes.append(clss) 53 | # debug 54 | # changed made here to match create_detections.py 55 | #boxes = np.squeeze(np.array(boxes)) 56 | #scores = np.squeeze(np.array(scores)) 57 | #classes = np.squeeze(np.array(classes)) 58 | 59 | return boxes,scores,classes 60 | 61 | 62 | 63 | 64 | 65 | # debug 66 | # added to generate detection for a single image 67 | def generate_detection_for_single_image(checkpoint,image): 68 | print("Creating Graph...") 69 | detection_graph = tf.Graph() 70 | with detection_graph.as_default(): 71 | od_graph_def = tf.GraphDef() 72 | with tf.gfile.GFile(checkpoint, 'rb') as fid: 73 | serialized_graph = fid.read() 74 | od_graph_def.ParseFromString(serialized_graph) 75 | tf.import_graph_def(od_graph_def, name='') 76 | 77 | #boxes = [] 78 | #scores = [] 79 | #classes = [] 80 | #k = 0 81 | with detection_graph.as_default(): 82 | with tf.Session(graph=detection_graph) as sess: 83 | # for image_np in tqdm(images): 84 | image_np_expanded = np.expand_dims(image, axis=0) 85 | image_tensor = detection_graph.get_tensor_by_name('image_tensor:0') 86 | box = detection_graph.get_tensor_by_name('detection_boxes:0') 87 | score = detection_graph.get_tensor_by_name('detection_scores:0') 88 | clss = detection_graph.get_tensor_by_name('detection_classes:0') 89 | num_detections = detection_graph.get_tensor_by_name('num_detections:0') 90 | # Actual detection. 91 | (box, score, clss, num_detections) = sess.run( 92 | [box, score, clss, num_detections], 93 | feed_dict={image_tensor: image_np_expanded}) 94 | 95 | # boxes.append(box) 96 | # scores.append(score) 97 | # classes.append(clss) 98 | 99 | #boxes = np.squeeze(np.array(boxes)) 100 | #scores = np.squeeze(np.array(scores)) 101 | #classes = np.squeeze(np.array(classes)) 102 | 103 | return box,score,clss 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /inference/harvey_label_map_2class.pbtxt: -------------------------------------------------------------------------------- 1 | item { 2 | id: 1 3 | name: 'Flooded / Damaged Building' 4 | } 5 | item { 6 | id: 2 7 | name: 'Non-damaged Building' 8 | } 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /inference/harvey_label_map_first.pbtxt: -------------------------------------------------------------------------------- 1 | item { 2 | id: 1 3 | name: 'Flooded / Damaged Building' 4 | } 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /inference/single_detection.sh: -------------------------------------------------------------------------------- 1 | python create_detections.py -c /home/ubuntu/anyan/models/research/models/harvey_ssd_inceptionv2_second/frozen_inference_graph_29774.pb -cs 512 /home/ubuntu/anyan/harvey_data/harvey_vis_result_toydata/20170831_105001000B95E200_3002321_jpeg_compressed_02_03.tif 2 | -------------------------------------------------------------------------------- /plot_bbox.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | 9 | ''' 10 | Take 2048 * 2048 chips, overlay bounding boxes with uids on them, 11 | and output png of the same size 12 | ''' 13 | 14 | import aug_util as aug 15 | import wv_util as wv 16 | import matplotlib.pyplot as plt 17 | import numpy as np 18 | import csv 19 | #%matplotlib inline 20 | #import matplotlib, copy, skimage, os, tifffile 21 | from skimage import io, morphology, draw 22 | import gdal 23 | from PIL import Image 24 | import random 25 | import json 26 | from tqdm import tqdm 27 | import io 28 | import glob 29 | import shutil 30 | import os 31 | 32 | 33 | 34 | # This is for single class Tomnod + Oak Ridge building foot print data (no offsets) 35 | def get_labels(fname): 36 | """ 37 | Gets label data from a geojson label file 38 | Args: 39 | fname: file path to an xView geojson label file 40 | Output: 41 | Returns three arrays: coords, chips, and classes corresponding to the 42 | coordinates, file-names, and classes for each ground truth. 43 | """ 44 | with open(fname) as f: 45 | data = json.load(f) 46 | 47 | coords = np.zeros((len(data['features']),4)) 48 | chips = np.zeros((len(data['features'])),dtype="object") 49 | classes = np.zeros((len(data['features']))) 50 | 51 | for i in tqdm(range(len(data['features']))): 52 | if data['features'][i]['properties']['bb'] != []: 53 | try: 54 | b_id = data['features'][i]['properties']['IMAGE_ID'] 55 | if b_id == '20170902_10400100324DAE00_3210111_jpeg_compressed_09_05.tif': 56 | print('found chip!') 57 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 58 | 59 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 60 | 61 | ymin = val[3] 62 | ymax = val[1] 63 | val[1] = ymin 64 | val[3] = ymax 65 | #print(val) 66 | chips[i] = str(b_id) 67 | 68 | classes[i] = data['features'][i]['properties']['TYPE_ID'] 69 | except: 70 | print('i:', i) 71 | print(data['features'][i]['properties']['IMAGE_ID']) 72 | #pass 73 | if val.shape[0] != 4: 74 | print("Issues at %d!" % i) 75 | else: 76 | coords[i] = val 77 | else: 78 | chips[i] = 'None' 79 | print('warning: chip is none') 80 | 81 | return coords, chips, classes 82 | 83 | 84 | # This is for 2 classes data: damaged + non-damaged buildings 85 | # Suitable for Tomnod + MS building footprints (no offsets) 86 | def get_labels_w_uid_nondamaged(fname): 87 | """ 88 | Gets label data from a geojson label file 89 | Args: 90 | fname: file path to an xView geojson label file 91 | Output: 92 | Returns three arrays: coords, chips, and classes corresponding to the 93 | coordinates, file-names, and classes for each ground truth. 94 | """ 95 | # debug 96 | # x_off = 15 97 | # y_off = 15 98 | # right_shift = 5 # how much shift to the right 99 | # add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 100 | with open(fname) as f: 101 | data = json.load(f) 102 | 103 | coords = np.zeros((len(data['features']),4)) 104 | chips = np.zeros((len(data['features'])),dtype="object") 105 | classes = np.zeros((len(data['features']))) 106 | # debug 107 | uids = np.zeros((len(data['features']))) 108 | 109 | for i in tqdm(range(len(data['features']))): 110 | if data['features'][i]['properties']['bb'] != []: 111 | try: 112 | b_id = data['features'][i]['properties']['Joined lay'] 113 | # if b_id == '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif': 114 | # print('found chip!') 115 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 116 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 117 | 118 | # ymin = val[3] 119 | # ymax = val[1] 120 | # val[1] = ymin 121 | # val[3] = ymax 122 | chips[i] = b_id 123 | classes[i] = data['features'][i]['properties']['type'] 124 | # debug 125 | uids[i] = int(data['features'][i]['properties']['uniqueid']) 126 | except: 127 | # print('i:', i) 128 | # print(data['features'][i]['properties']['bb']) 129 | pass 130 | if val.shape[0] != 4: 131 | print("Issues at %d!" % i) 132 | else: 133 | coords[i] = val 134 | else: 135 | chips[i] = 'None' 136 | # debug 137 | # added offsets to each coordinates 138 | # need to check the validity of bbox maybe 139 | #coords = np.add(coords, add_np) 140 | 141 | return coords, chips, classes, uids 142 | 143 | 144 | 145 | 146 | 147 | def draw_bbox_on_tiff(chip_path, coords, chips, classes,uids, save_path): 148 | #Load an image 149 | #path = '/home/ubuntu/anyan/harvey_data/converted_sample_tiff/' 150 | 151 | 152 | # big tiff name: chip name 153 | # init to {big_tiff_name : []} 154 | #big_tiff_dict = dict((k, []) for k in big_tiff_set) 155 | #big_tiff_dict = dict() 156 | 157 | fnames = glob.glob(chip_path + "*.tif") 158 | i = 0 159 | for f in fnames: 160 | 161 | chip_name = f.split('/')[-1].strip() 162 | chip_big_tiff_id_list = chip_name.split('_')[1:3] 163 | chip_big_tiff_id = '_'.join(chip_big_tiff_id_list) 164 | #print(chip_big_tiff_id) 165 | ''' 166 | if chip_big_tiff_id not in set(big_tiff_dict.keys()): 167 | big_tiff_dict[chip_big_tiff_id] = list() 168 | big_tiff_dict[chip_big_tiff_id].append(chip_name) 169 | else: 170 | big_tiff_dict[chip_big_tiff_id].append(chip_name) 171 | 172 | if len(big_tiff_dict[chip_big_tiff_id]) > 5: 173 | continue 174 | ''' 175 | # debug 176 | print(chip_big_tiff_id) 177 | #big_tiff_dict[chip_big_tiff_id].append(chip_name) 178 | arr = wv.get_image(f) 179 | # print(arr.shape) 180 | # plt.figure(figsize=(10,10)) 181 | # plt.axis('off') 182 | # plt.imshow(arr) 183 | coords_chip = coords[chips==chip_name] 184 | #print(chip_name) 185 | #print(coords_chip.shape) 186 | if coords_chip.shape[0] == 0: 187 | print('no bounding boxes in this image') 188 | print(chip_name) 189 | continue 190 | classes_chip = classes[chips==chip_name].astype(np.int64) 191 | # #We can chip the image into 500x500 chips 192 | # c_img, c_box, c_cls = wv.chip_image(img = arr, coords= coords, classes=classes, shape=(500,500)) 193 | # print("Num Chips: %d" % c_img.shape[0]) 194 | uids_chip = uids[chips == chip_name].astype(np.int64) 195 | labelled = aug.draw_bboxes_withindex_multiclass(arr,coords_chip,classes_chip, uids_chip) 196 | print(chip_name) 197 | # plt.figure(figsize=(15,15)) 198 | # plt.axis('off') 199 | # plt.imshow(labelled) 200 | subdir_name = save_path + chip_big_tiff_id 201 | if os.path.isdir(subdir_name): 202 | save_name = subdir_name +'/' + chip_name + '.png' 203 | print('saving image: ', save_name) 204 | labelled.save(save_name) 205 | else: 206 | os.mkdir(subdir_name) 207 | save_name = subdir_name +'/' + chip_name + '.png' 208 | print('saving image: ',save_name) 209 | labelled.save(save_name) 210 | 211 | 212 | #else: 213 | #continue 214 | 215 | #debug 216 | #print('len of big_tiff_dict: ', len(big_tiff_dict.keys())) 217 | 218 | #chip_name = '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif' 219 | # chip_name = '20170831_105001000B95E100_3020021_jpeg_compressed_04_04.tif' 220 | #chip_name = '20170831_105001000B95E100_3020021_jpeg_compressed_05_02.tif' 221 | #chip_name = '20170831_105001000B95E100_3020021_jpeg_compressed_05_04.tif' 222 | #chip_name = '20170831_105001000B95E100_3020021_jpeg_compressed_06_02.tif' 223 | #chip_fullname = path + chip_name 224 | #print(chip_fullname) 225 | 226 | 227 | #labelled.save("test.png") 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | def main(): 236 | 237 | 238 | #geojson_file = '../bounding_box_referenced_2.geojson' 239 | #geojson_file = '../harvey_test_second.geojson' 240 | geojson_file = '../bboxes_tomnod_2class_v1.geojson' 241 | coords, chips, classes, uids = wv.get_labels_w_uid_nondamaged(geojson_file) 242 | print('number of chips is :', chips.shape) 243 | test_tif = '20170902_10400100324DAE00_3210111_jpeg_compressed_09_05.tif' 244 | if test_tif in chips.tolist(): 245 | print('test tif exists!!!!!') 246 | 247 | #print('chips, ', chips.tolist()) 248 | #path = '/home/ubuntu/anyan/harvey_data/harvey_test_second/' 249 | #save_path = '/home/ubuntu/anyan/harvey_data/inspect_black_in_test/' 250 | path = '../harvey_vis_result_toydata/' 251 | save_path = '../harvey_vis_result_toydata_bboxes/' 252 | 253 | #aug.draw_bboxes_withindex(arr,coords_chip, uids_chip) 254 | draw_bbox_on_tiff(path, coords, chips, classes,uids, save_path) 255 | 256 | if __name__ == '__main__': 257 | main() 258 | -------------------------------------------------------------------------------- /plot_bbox_uid_small.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Defense Innovation Unit Experimental 3 | All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Modifications copyright (C) 2018 18 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 19 | Written by An Yan 20 | 21 | 22 | """ 23 | 24 | 25 | 26 | ''' 27 | This script is modified from process_wv.py to plot bounding 28 | boxes with uids on chipped tiles (i.e., 256 x 256 / 512 x 512) for manual inspection 29 | 30 | bboxes over clouds are removed automatically by naive scripts. These 31 | removed uids will be recorded and written to a csv/txt file 32 | 33 | The rest of the bboxes will be rendered to png files, which will be 34 | handed to manual inspection 35 | 36 | ''' 37 | 38 | 39 | 40 | from PIL import Image 41 | import tensorflow as tf 42 | import io 43 | import glob 44 | from tqdm import tqdm 45 | import numpy as np 46 | import logging 47 | import argparse 48 | import os 49 | import json 50 | import wv_util as wv 51 | import tfr_util as tfr 52 | import aug_util as aug 53 | import csv 54 | 55 | """ 56 | A script that processes xView imagery. 57 | Args: 58 | image_folder: A folder path to the directory storing xView .tif files 59 | ie ("xView_data/") 60 | 61 | json_filepath: A file path to the GEOJSON ground truth file 62 | ie ("xView_gt.geojson") 63 | 64 | test_percent (-t): The percentage of input images to use for test set 65 | 66 | suffix (-s): The suffix for output TFRecord files. Default suffix 't1' will output 67 | xview_train_t1.record and xview_test_t1.record 68 | 69 | augment (-a): A boolean value of whether or not to use augmentation 70 | 71 | Outputs: 72 | Writes two files to the current directory containing training and test data in 73 | TFRecord format ('xview_train_SUFFIX.record' and 'xview_test_SUFFIX.record') 74 | """ 75 | 76 | 77 | 78 | 79 | def detect_blackblock(img): 80 | # check the # of pixels that with RGB values are all equal to 0 81 | w,h,c = img.shape 82 | black_pixel_count=0 83 | threshold = 0.9 * w * h * 3 84 | non_black_count = np.count_nonzero(img) 85 | if non_black_count > threshold: 86 | return False 87 | else: 88 | return True 89 | 90 | 91 | 92 | def detect_clouds(img, boxes, classes, uids): 93 | mean_threshold_min = 160 94 | w, h, _ = img.shape 95 | #print('w,h', w, h) 96 | 97 | # TODO: try the threshold, process_wv.py used 18 98 | var_threshold = 25 99 | rows_to_delete = list() 100 | deleted_uids = list() 101 | for i in range(boxes.shape[0]): 102 | xmin, ymin, xmax, ymax = boxes[i] 103 | # ymin = 0 104 | if xmin < 0: 105 | xmin = 0 106 | if ymin<0: 107 | y_min = 0 108 | if xmax > h: 109 | print('xmax > h') 110 | xmax = h 111 | if ymax > w: 112 | print('ymax > w') 113 | ymax= h 114 | #print(xmin, ymin, xmax, ymax) 115 | # clip bbox areas 116 | #cropped_img = img.crop((xmin, ymin, xmax, ymax)) 117 | cropped_img = img[int(ymin):int(ymax), int(xmin):int(xmax)] # note the order of w/h 118 | # print(cropped_img) 119 | # print(cropped_img.shape) 120 | array_img = np.array(cropped_img) 121 | mean_img = np.mean(array_img) 122 | 123 | #print('mean_img', mean_img, i) 124 | var_img = np.std(array_img) 125 | #print('var_img',var_img, i) 126 | #if var_img < var_threshold and (cropped_img> 150).all() and (cropped_img< 255).all(): 127 | if var_img < var_threshold and mean_img > mean_threshold_min: 128 | print('bounding box i has cloud', i) 129 | # need to delete this bbox 130 | rows_to_delete.append(i) 131 | deleted_uids.append(uids[i]) 132 | print('rows_to_delete',rows_to_delete) 133 | 134 | if len(rows_to_delete) == 0: 135 | return img, boxes, classes, uids, [] 136 | else: 137 | # return boxes and classes with clouds removed 138 | new_coords = np.delete(boxes, rows_to_delete, axis=0) 139 | new_classes = np.delete(classes, rows_to_delete, axis=0) 140 | new_uids = np.delete(uids, rows_to_delete, axis=0) 141 | return img, new_coords, new_classes, new_uids, deleted_uids 142 | 143 | 144 | 145 | 146 | 147 | 148 | def get_images_from_filename_array(coords,chips,classes,folder_names,res=(250,250)): 149 | """ 150 | Gathers and chips all images within a given folder at a given resolution. 151 | 152 | Args: 153 | coords: an array of bounding box coordinates 154 | chips: an array of filenames that each coord/class belongs to. 155 | classes: an array of classes for each bounding box 156 | folder_names: a list of folder names containing images 157 | res: an (X,Y) tuple where (X,Y) are (width,height) of each chip respectively 158 | 159 | Output: 160 | images, boxes, classes arrays containing chipped images, bounding boxes, and classes, respectively. 161 | """ 162 | 163 | images =[] 164 | boxes = [] 165 | clses = [] 166 | 167 | k = 0 168 | bi = 0 169 | 170 | for folder in folder_names: 171 | fnames = glob.glob(folder + "*.tif") 172 | fnames.sort() 173 | for fname in tqdm(fnames): 174 | #Needs to be "X.tif" ie ("5.tif") 175 | name = fname.split("\\")[-1] 176 | arr = wv.get_image(fname) 177 | 178 | img,box,cls = wv.chip_image(arr,coords[chips==name],classes[chips==name],res) 179 | 180 | for im in img: 181 | images.append(im) 182 | for b in box: 183 | boxes.append(b) 184 | for c in cls: 185 | clses.append(cls) 186 | k = k + 1 187 | 188 | return images, boxes, clses 189 | 190 | def shuffle_images_and_boxes_classes(im,box,cls): 191 | """ 192 | Shuffles images, boxes, and classes, while keeping relative matching indices 193 | 194 | Args: 195 | im: an array of images 196 | box: an array of bounding box coordinates ([xmin,ymin,xmax,ymax]) 197 | cls: an array of classes 198 | 199 | Output: 200 | Shuffle image, boxes, and classes arrays, respectively 201 | """ 202 | assert len(im) == len(box) 203 | assert len(box) == len(cls) 204 | 205 | perm = np.random.permutation(len(im)) 206 | out_b = {} 207 | out_c = {} 208 | 209 | k = 0 210 | for ind in perm: 211 | out_b[k] = box[ind] 212 | out_c[k] = cls[ind] 213 | k = k + 1 214 | return im[perm], out_b, out_c 215 | 216 | ''' 217 | Datasets 218 | _multires: multiple resolutions. Currently [(500,500),(400,400),(300,300),(200,200)] 219 | _aug: Augmented dataset 220 | ''' 221 | 222 | if __name__ == "__main__": 223 | parser = argparse.ArgumentParser() 224 | parser.add_argument("image_folder", help="Path to folder containing image chips (ie 'Image_Chips/' ") 225 | parser.add_argument("json_filepath", help="Filepath to GEOJSON coordinate file") 226 | parser.add_argument("-t", "--test_percent", type=float, default=0.333, 227 | help="Percent to split into test (ie .25 = test set is 25% total)") 228 | parser.add_argument("-s", "--suffix", type=str, default='t1', 229 | help="Output TFRecord suffix. Default suffix 't1' will output 'xview_train_t1.record' and 'xview_test_t1.record'") 230 | parser.add_argument("-a","--augment", type=bool, default=False, 231 | help="A boolean value whether or not to use augmentation") 232 | # debug: added percent of data to produce, the purpose is to produce small dataset for fast algorithm development 233 | parser.add_argument("-p", "--sample_percent", type=int, default = 1, help = "Portion to sample data (1/sample_percent) from the original dataset. Meaning that only use a portion of the dataset to construct training and testing. The purpose is for fast algorithm development") 234 | 235 | 236 | 237 | args = parser.parse_args() 238 | 239 | logging.basicConfig(level=logging.INFO) 240 | logger = logging.getLogger(__name__) 241 | 242 | #resolutions should be largest -> smallest. We take the number of chips in the largest resolution and make 243 | #sure all future resolutions have less than 1.5times that number of images to prevent chip size imbalance. 244 | #res = [(500,500),(400,400),(300,300),(200,200)] 245 | #res = [(300,300)] 246 | #res = [(512,512)] 247 | 248 | res = [(512, 512)] 249 | 250 | 251 | AUGMENT = args.augment 252 | # debug 253 | #SAVE_IMAGES = False 254 | SAVE_IMAGES = True 255 | images = {} 256 | boxes = {} 257 | train_chips = 0 258 | test_chips = 0 259 | 260 | 261 | # debug 262 | deleted_uids_bycloud = set() 263 | 264 | #Parameters 265 | max_chips_per_res = 100000 266 | train_writer = tf.python_io.TFRecordWriter("harvey_delete1_%s.record" % args.suffix) 267 | test_writer = tf.python_io.TFRecordWriter("harvey_delete2_%s.record" % args.suffix) 268 | 269 | #coords,chips,classes = wv.get_labels(args.json_filepath) 270 | coords,chips,classes,uids = wv.get_labels_w_uid_nondamaged(args.json_filepath) 271 | 272 | 273 | # debug 274 | #print('number of chips from geojson', len(chips)) 275 | #print('number of classes from geojson', len(classes)) 276 | #print('some coords: ', coords[2]) 277 | #print('some coords: ', coords[3000]) 278 | #print('classes some: ', classes[4]) 279 | #print('chips ', chips[349]) 280 | 281 | # debug 282 | sample_percent = args.sample_percent 283 | # a list of classes to be augment. Set to set to be empty if no augmentation 284 | # is wanted 285 | class_to_aug = set([]) 286 | num_aug_per_class = {} # class_id: # of augmentation generated 287 | for class_id in class_to_aug: 288 | num_aug_per_class[class_id] = 0 289 | 290 | 291 | #debug 292 | # for cloud removing and black portion removing 293 | num_cloud_rm = 0 # number of 512 x 512 chips that have clouds removed 294 | num_black = 0 # number of 512 x 512 chips that have black parts 295 | 296 | 297 | 298 | for res_ind, it in enumerate(res): 299 | tot_box = 0 300 | logging.info("Res: %s" % str(it)) 301 | ind_chips = 0 302 | 303 | fnames = glob.glob(args.image_folder + "*.tif") 304 | fnames.sort() 305 | 306 | for fname in tqdm(fnames): 307 | #Needs to be "X.tif", ie ("5.tif") 308 | #Be careful!! Depending on OS you may need to change from '/' to '\\'. Use '/' for UNIX and '\\' for windows 309 | name = fname.split("/")[-1] 310 | # debug 311 | #print('file name: ', name) 312 | arr = wv.get_image(fname) 313 | 314 | # debug 315 | print('file name: ', name) 316 | #print('classes[chips==name], ', classes[chips==name]) 317 | 318 | 319 | im,box,classes_final, uids_chip = wv.chip_image_with_uid(arr,coords[chips==name],classes[chips==name],uids[chips==name] ,it) 320 | 321 | #Shuffle images & boxes all at once. Comment out the line below if you don't want to shuffle images 322 | #im,box,classes_final = shuffle_images_and_boxes_classes(im,box,classes_final) 323 | split_ind = int(im.shape[0] * args.test_percent) 324 | 325 | for idx, image in enumerate(im): 326 | if idx%sample_percent !=0: 327 | continue 328 | # debug 329 | print('processing idx: ', idx) 330 | # debug 331 | # remove black block 332 | if detect_blackblock(image): 333 | num_black +=1 334 | continue 335 | # remove clouds 336 | 337 | image, new_coords, new_classes, new_uids, deleted_uids = detect_clouds(image,box[idx],classes_final[idx], uids_chip[idx]) 338 | if len(new_coords)!= len(box[idx]): 339 | num_cloud_rm += 1 340 | deleted_uids_bycloud = deleted_uids_bycloud.union(set(deleted_uids)) 341 | 342 | # debug: changed image,box[idx],classes_final[idx] to newly constructed img and box 343 | # tf_example = tfr.to_tf_example(image,box[idx],classes_final[idx]) 344 | tf_example = tfr.to_tf_example(image, new_coords, new_classes) 345 | 346 | 347 | 348 | 349 | #Check to make sure that the TF_Example has valid bounding boxes. 350 | #If there are no valid bounding boxes, then don't save the image to the TFRecord. 351 | float_list_value_xmin = tf_example.features.feature['image/object/bbox/xmin'].float_list.value 352 | float_list_value_ymin = tf_example.features.feature['image/object/bbox/ymin'].float_list.value 353 | float_list_value_xmax = tf_example.features.feature['image/object/bbox/xmax'].float_list.value 354 | float_list_value_ymax = tf_example.features.feature['image/object/bbox/ymax'].float_list.value 355 | 356 | if (ind_chips < max_chips_per_res and np.array(float_list_value_xmin).any() and np.array(float_list_value_xmax).any() and np.array(float_list_value_ymin).any() and np.array(float_list_value_ymax).any()): 357 | tot_box+=np.array(float_list_value_xmin).shape[0] 358 | 359 | if idx < split_ind: 360 | test_writer.write(tf_example.SerializeToString()) 361 | test_chips+=1 362 | #if SAVE_IMAGES: 363 | # debug: changed save dir 364 | # aug.draw_bboxes(image, new_coords).save('./harvey_img_inspect_uid_val/img_%s_%s.png'%(name,str(idx))) 365 | 366 | 367 | else: 368 | train_writer.write(tf_example.SerializeToString()) 369 | train_chips += 1 370 | #if SAVE_IMAGES: 371 | # debug: changed save dir 372 | # aug.draw_bboxes(image, new_coords).save('./harvey_img_inspect_uid_train/img_%s_%s.png'%(name,str(idx))) 373 | 374 | 375 | 376 | 377 | ind_chips +=1 378 | 379 | # debug 380 | # store the training and validation images with bboxes for inspection 381 | 382 | #if SAVE_IMAGES: 383 | # debug: changed save dir 384 | # aug.draw_bboxes_withindex_multiclass(image, new_coords, new_classes, new_uids).save('../harvey_manual_inspect_test_512/img_%s_%s.png'%(name,str(idx))) 385 | 386 | 387 | 388 | 389 | #Make augmentation probability proportional to chip size. Lower chip size = less chance. 390 | #This makes the chip-size imbalance less severe. 391 | prob = np.random.randint(0,np.max(res)) 392 | #for 200x200: p(augment) = 200/500 ; for 300x300: p(augment) = 300/500 ... 393 | 394 | 395 | 396 | # debug 397 | # added customized data augmentation for minor classes 398 | #class_to_aug = [2, 3, 4] # damaged roads, trash heaps, and bridges 399 | # Minor classes will be augmented to 63 times larger with various augmentations 400 | # 1. Detect whether minor classes are in the small chips, if yes, augment 401 | # this chip. The output will be a tensor of augmented images, bboxes, and classes 402 | # unpack the output to tfrecord TRAINING data. 403 | # 2. If the chip does not contain any minor classes, go to normal augmentation 404 | #skip_augmentation = set() # contains a list of chips that contain minor classes 405 | MINOR_CLASS_FLAG = False 406 | for class_id in class_to_aug: 407 | #num_aug_per_class[class_id] = 0 408 | #num_aug_this_class = 0 409 | # debug 410 | # print('checking whether this chip contain class: ', class_id) 411 | # this chip contains minor classes 412 | #if np.any(classes_final[idx][:]== class_id): 413 | #if class_id in set(classes_final[idx]) and idx > split_ind: 414 | if class_id in set(new_classes) and idx > split_ind: 415 | # skip_augmentation.add(idx) 416 | MINOR_CLASS_FLAG = True 417 | # print('trying to call expand_aug for chip: ', idx) 418 | #im_aug,boxes_aug,classes_aug= aug.expand_aug_random(image, box[idx], classes_final[idx], class_id) 419 | im_aug,boxes_aug,classes_aug= aug.expand_aug_random(image, new_coords, new_classes, class_id) 420 | 421 | 422 | #debug 423 | print('augmentig chip: ', idx) 424 | num_aug = 0 425 | for aug_idx, aug_image in enumerate(im_aug): 426 | tf_example_aug = tfr.to_tf_example(aug_image, boxes_aug[aug_idx],classes_aug[aug_idx]) 427 | #Check to make sure that the TF_Example has valid bounding boxes. 428 | #If there are no valid bounding boxes, then don't save the image to the TFRecord. 429 | float_list_value_xmin = tf_example_aug.features.feature['image/object/bbox/xmin'].float_list.value 430 | float_list_value_xmax = tf_example_aug.features.feature['image/object/bbox/xmax'].float_list.value 431 | float_list_value_ymin = tf_example_aug.features.feature['image/object/bbox/ymin'].float_list.value 432 | float_list_value_ymax = tf_example_aug.features.feature['image/object/bbox/ymax'].float_list.value 433 | 434 | # debug 435 | #num_aug = 0 436 | if (np.array(float_list_value_xmin).any() and np.array(float_list_value_xmax).any() and np.array(float_list_value_ymin).any() and np.array(float_list_value_ymax).any()): 437 | tot_box+=np.array(float_list_value_xmin).shape[0] 438 | 439 | train_writer.write(tf_example_aug.SerializeToString()) 440 | num_aug = num_aug + 1 441 | train_chips+=1 442 | num_aug_per_class[class_id] = num_aug_per_class[class_id]+1 443 | # num_aug_this_class=num_aug_this_class + 1 444 | # debug 445 | if aug_idx%10 == 0 and SAVE_IMAGES: 446 | # debug: changed save dir 447 | aug_image = (aug_image).astype(np.uint8) 448 | aug.draw_bboxes(aug_image,boxes_aug[aug_idx]).save('./expand_aug_random_256/img_aug_%s_%s_%s_%s.png'%(name, str(idx), str(aug_idx), str(class_id))) 449 | # debug 450 | print('augmenting class: ', class_id) 451 | print('number of augmentation: ',num_aug) 452 | #num_aug_per_class[class_id] = num_aug_this_class 453 | 454 | # it: iterator for different resolutions 455 | # start to augment the rest 456 | if AUGMENT and prob < it[0] and MINOR_CLASS_FLAG == False: 457 | 458 | for extra in range(3): 459 | center = np.array([int(image.shape[0]/2),int(image.shape[1]/2)]) 460 | deg = np.random.randint(-10,10) 461 | #deg = np.random.normal()*30 462 | # changed 463 | # remove and gaussian blur 464 | 465 | newimg = aug.gaussian_blur(image) 466 | #newimg = image 467 | 468 | #.3 probability for each of shifting vs rotating vs shift(rotate(image)) 469 | p = np.random.randint(0,3) 470 | # debug 471 | # modified to use the removed cloud version of bboxes 472 | # image, new_coords, new_classes 473 | if p == 0: 474 | newimg,nb = aug.shift_image(newimg,new_coords) 475 | #newimg,nb = aug.shift_image(newimg,box[idx]) 476 | elif p == 1: 477 | newimg,nb = aug.rotate_image_and_boxes(newimg,deg,center,new_coords) 478 | #newimg,nb = aug.rotate_image_and_boxes(newimg,deg,center,box[idx]) 479 | elif p == 2: 480 | newimg,nb = aug.rotate_image_and_boxes(newimg,deg,center,new_coords) 481 | #newimg,nb = aug.rotate_image_and_boxes(newimg,deg,center,box[idx]) 482 | newimg,nb = aug.shift_image(newimg,nb) 483 | 484 | 485 | newimg = (newimg).astype(np.uint8) 486 | 487 | if idx%100 == 0 and SAVE_IMAGES: 488 | #debug 489 | # changed save dir 490 | Image.fromarray(newimg).save('./augmented_img_60/img_%s_%s_%s.png'%(name,extra,it[0])) 491 | 492 | if len(nb) > 0: 493 | # debug 494 | # modified to use the cloud removed bboxs 495 | tf_example = tfr.to_tf_example(newimg,nb,new_classes) 496 | #tf_example = tfr.to_tf_example(newimg,nb,classes_final[idx]) 497 | 498 | #DonI't count augmented chips for chip indices 499 | # changed 500 | # removed data augmentation for test data 501 | if idx < split_ind: 502 | # test_writer.write(tf_example.SerializeToString()) 503 | # test_chips += 1 504 | continue 505 | else: 506 | train_writer.write(tf_example.SerializeToString()) 507 | train_chips+=1 508 | # debug: 509 | # save image + bounding boxes for debug 510 | #else: 511 | if idx%100 ==0 and SAVE_IMAGES: 512 | # debug: changed save dir 513 | aug.draw_bboxes(newimg,nb).save('./harvey_augmented/img_aug_%s_%s_%s.png'%(name,extra,it[0])) 514 | if res_ind == 0: 515 | max_chips_per_res = int(ind_chips * 1.5) 516 | logging.info("Max chips per resolution: %s " % max_chips_per_res) 517 | 518 | logging.info("Tot Box: %d" % tot_box) 519 | logging.info("Chips: %d" % ind_chips) 520 | 521 | # debug 522 | for key, val in num_aug_per_class.items(): 523 | print('for class:' , key) 524 | print('augmentation applied: ', val) 525 | # debug 526 | print('num of black small chips removed: ', num_black) 527 | print('num of small chips containing clouds:', num_cloud_rm) 528 | 529 | logging.info("saved: %d train chips" % train_chips) 530 | logging.info("saved: %d test chips" % test_chips) 531 | train_writer.close() 532 | test_writer.close() 533 | print('number of bboxes over clouds deleted: ', len(deleted_uids_bycloud)) 534 | # write deleted_uids_bycloud to file 535 | cloudfname = 'automatic_deleted_bbox_onclouds_' + args.suffix + '.txt' 536 | with open(cloudfname, 'w') as f: 537 | for uid in deleted_uids_bycloud: 538 | f.write('%s\n'% int(uid)) 539 | f.close() 540 | 541 | 542 | 543 | -------------------------------------------------------------------------------- /scoring/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-onbuild 2 | 3 | -------------------------------------------------------------------------------- /scoring/__pycache__/matching.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/scoring/__pycache__/matching.cpython-36.pyc -------------------------------------------------------------------------------- /scoring/__pycache__/rectangle.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/scoring/__pycache__/rectangle.cpython-36.pyc -------------------------------------------------------------------------------- /scoring/evaluation.py: -------------------------------------------------------------------------------- 1 | """Utility methods for computing the performance metrics.""" 2 | from __future__ import division 3 | from matching import Matching 4 | from rectangle import Rectangle 5 | 6 | 7 | def safe_divide(numerator, denominator): 8 | """Computes the safe division to avoid the divide by zero problem.""" 9 | if denominator == 0: 10 | return 0 11 | return numerator / denominator 12 | 13 | 14 | def compute_statistics_given_rectangle_matches(groundtruth_rects_matched, rects_matched): 15 | """Computes the staticstics given the groundtruth_rects and rects matches. 16 | 17 | Args: 18 | image_id: the image_id referring to the image to be evaluated. 19 | groundtruth_rects_matched: the groundtruth_rects_matched represents 20 | a list of integers returned from the Matching class instance to 21 | indicate the matched rectangle indices from rects for each of the 22 | groundtruth_rects. 23 | rects_matched: the rects_matched represents a list of integers returned 24 | from the Matching class instance to indicate the matched rectangle 25 | indices from groundtruth_rects for each of the rects. 26 | Returns: 27 | A dictionary holding the computed statistics as well as the inputs. 28 | """ 29 | # Calculate the total_positives, true_positives, and false_positives. 30 | total_positives = len(groundtruth_rects_matched) 31 | true_positives = sum(item is not None for item in groundtruth_rects_matched) 32 | false_positives = sum(item is None for item in rects_matched) 33 | return {'groundtruth_rects_matched': groundtruth_rects_matched, 34 | 'rects_matched': rects_matched, 35 | 'total_positives': total_positives, 36 | 'true_positives': true_positives, 37 | 'false_positives': false_positives} 38 | 39 | 40 | def compute_precision_recall_given_image_statistics_list( 41 | iou_threshold, image_statistics_list): 42 | """Computes the precision recall numbers given iou_threshold and statistics. 43 | 44 | Args: 45 | iou_threshold: the iou_threshold under which the statistics are computed. 46 | image_statistics_list: a list of the statistics computed and returned 47 | by the compute_statistics_given_rectangle_matches method for a list of 48 | images. 49 | Returns: 50 | A dictionary holding the precision, recall as well as the inputs. 51 | """ 52 | total_positives = 0 53 | true_positives = 0 54 | false_positives = 0 55 | for statistics in image_statistics_list: 56 | total_positives += statistics['total_positives'] 57 | true_positives += statistics['true_positives'] 58 | false_positives += statistics['false_positives'] 59 | precision = safe_divide(true_positives, true_positives + false_positives) 60 | recall = safe_divide(true_positives, total_positives) 61 | return {'iou_threshold': iou_threshold, 62 | 'precision': precision, 63 | 'recall': recall, 64 | 'image_statistics_list': image_statistics_list} 65 | 66 | 67 | def compute_average_precision_recall_given_precision_recall_dict( 68 | precision_recall_dict): 69 | """Computes the average precision (AP) and average recall (AR). 70 | 71 | Args: 72 | precision_recall_dict: the precision_recall_dict holds the dictionary of 73 | precision and recall information returned by the 74 | compute_precision_recall_given_image_statistics_list method, which is 75 | calcualted under a range of iou_thresholds, where the iou_threshold is 76 | the key. 77 | Returns: 78 | average_precision, average_recall. 79 | """ 80 | precision = 0 81 | recall = 0 82 | for _, value in precision_recall_dict.items(): 83 | precision += value['precision'] 84 | recall += value['recall'] 85 | average_precision = safe_divide(precision, len(precision_recall_dict)) 86 | average_recall = safe_divide(recall, len(precision_recall_dict)) 87 | return average_precision, average_recall 88 | 89 | 90 | def convert_to_rectangle_list(coordinates): 91 | """Converts the coordinates in a list to the Rectangle list.""" 92 | rectangle_list = [] 93 | number_of_rects = int(len(coordinates) / 4) 94 | for i in range(number_of_rects): 95 | rectangle_list.append(Rectangle( 96 | coordinates[4 * i], coordinates[4 * i + 1], coordinates[4 * i + 2], 97 | coordinates[4 * i + 3])) 98 | return rectangle_list 99 | 100 | 101 | def compute_average_precision_recall( 102 | groundtruth_coordinates, coordinates, iou_threshold): 103 | """Computes the average precision (AP) and average recall (AR). 104 | 105 | Args: 106 | groundtruth_info_dict: the groundtruth_info_dict holds all the groundtruth 107 | information for an evaluation dataset. The format of this groundtruth_info_dict is 108 | as follows: 109 | {'image_id_0': 110 | [xmin_0,ymin_0,xmax_0,ymax_0,...,xmin_N0,ymin_N0,xmax_N0,ymax_N0], 111 | ..., 112 | 'image_id_M': 113 | [xmin_0,ymin_0,xmax_0,ymax_0,...,xmin_NM,ymin_NM,xmax_NM,ymax_NM]}, 114 | where 115 | image_id_* is an image_id that has the groundtruth rectangles labeled. 116 | xmin_*,ymin_*,xmax_*,ymax_* is the top-left and bottom-right corners 117 | of one groundtruth rectangle. 118 | 119 | test_info_dict: the test_info_dict holds all the test information for an 120 | evaluation dataset. 121 | The format of this test_info_dict is the same 122 | as the above groundtruth_info_dict. 123 | 124 | iou_threshold_range: the IOU threshold range to compute the average 125 | precision (AP) and average recall (AR). For example: 126 | iou_threshold_range = [0.50:0.05:0.95] 127 | Returns: 128 | average_precision, average_recall, as well as the precision_recall_dict, 129 | where precision_recall_dict holds the full precision/recall information 130 | for each of the iou_threshold in the iou_threshold_range. 131 | Raises: 132 | ValueError: if the input groundtruth_info_dict and test_info_dict show 133 | inconsistent information. 134 | """ 135 | 136 | # Start to build up the Matching instances for each of the image_id_*, which 137 | # is to hold the IOU computation between the rectangle pairs for the same 138 | # image_id_*. 139 | matchings = {} 140 | if (len(groundtruth_coordinates) % 4 != 0) or (len(coordinates) % 4 != 0): 141 | raise ValueError('groundtruth_info_dict and test_info_dict should hold ' 142 | 'only 4 * N numbers.') 143 | 144 | groundtruth_rects = convert_to_rectangle_list(groundtruth_coordinates) 145 | rects = convert_to_rectangle_list(coordinates) 146 | matching = Matching(groundtruth_rects, rects) 147 | 148 | image_statistics_list = [] 149 | groundtruth_rects_matched, rects_matched = ( 150 | matching.matching_by_greedy_assignment(iou_threshold)) 151 | 152 | image_statistics = compute_statistics_given_rectangle_matches( 153 | groundtruth_rects_matched, rects_matched) 154 | image_statistics_list.append(image_statistics) 155 | 156 | # Compute the precision and recall under this iou_threshold. 157 | precision_recall = compute_precision_recall_given_image_statistics_list( 158 | iou_threshold, image_statistics_list) 159 | 160 | # Compute the average_precision and average_recall. 161 | #average_precision, average_recall = ( 162 | # compute_average_precision_recall_given_precision_recall_dict( 163 | # precision_recall_dict)) 164 | 165 | return precision_recall 166 | -------------------------------------------------------------------------------- /scoring/matching.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Defense Innovation Unit Experimental 3 | All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | """ 17 | 18 | from collections import defaultdict 19 | from rectangle import Rectangle 20 | import numpy as np 21 | 22 | class Matching(object): 23 | """Matching class.""" 24 | 25 | def __init__(self, groundtruth_rects, rects): 26 | """Constructs a Matching instance. 27 | 28 | Args: 29 | groundtruth_rects: a list of groundtruth rectangles. 30 | rects: a list of rectangles to be matched against the groundtruth_rects. 31 | Raises: 32 | ValueError: if any item inside the groundtruth_rects or rects are not 33 | Rectangle type. 34 | """ 35 | for rect in groundtruth_rects: 36 | if not isinstance(rect, Rectangle): 37 | raise ValueError('Invalid instance type: should be Rectangle.') 38 | for rect in rects: 39 | if not isinstance(rect, Rectangle): 40 | raise ValueError('Invalid instance type: should be Rectangle.') 41 | self.groundtruth_rects_ = groundtruth_rects 42 | self.rects_ = rects 43 | self._compute_iou_from_rectangle_pairs() 44 | 45 | def _compute_iou_from_rectangle_pairs(self): 46 | """Computes the iou scores between all pairs of rectangles.""" 47 | #try to extract a matrix nx4 from rects 48 | m = len(self.groundtruth_rects_) 49 | n = len(self.rects_) 50 | self.n = n 51 | self.m = m 52 | 53 | self.iou_rectangle_pair_indices_ = defaultdict(list) 54 | 55 | if not(n == 0 or m == 0): 56 | 57 | mat2 = np.array( [j.coords for j in self.groundtruth_rects_]) 58 | mat1 = np.array([j.coords for j in self.rects_]) 59 | #i,j axes correspond to #boxes, #coords per rect 60 | 61 | #compute the areas 62 | w1 = mat1[:,2] - mat1[:,0] 63 | w2 = mat2[:,2] - mat2[:,0] 64 | h1 = mat1[:,3] - mat1[:,1] 65 | h2 = mat2[:,3] - mat2[:,1] 66 | a1 = np.multiply(h1,w1) 67 | a2 = np.multiply(h2,w2) 68 | w_h_matrix = cartesian([a1,a2]).reshape((n,m,2)) 69 | a_matrix = w_h_matrix.sum(axis=2) 70 | 71 | #now calculate the intersection rectangle 72 | i_xmin = cartesian([mat1[:,0],mat2[:,0]]).reshape((n,m,2)) 73 | i_xmax = cartesian([mat1[:,2],mat2[:,2]]).reshape((n,m,2)) 74 | i_ymin = cartesian([mat1[:,1],mat2[:,1]]).reshape((n,m,2)) 75 | i_ymax = cartesian([mat1[:,3],mat2[:,3]]).reshape((n,m,2)) 76 | i_w = np.min(i_xmax,axis=2) - np.max(i_xmin,axis=2) 77 | i_h = np.min(i_ymax,axis=2) - np.max(i_ymin,axis=2) 78 | i_w[i_w < 0] = 0 79 | i_h[i_h < 0] = 0 80 | 81 | i_a_matrix = np.multiply(i_w,i_h) 82 | iou_matrix = np.divide(i_a_matrix, (a_matrix - i_a_matrix)) 83 | self.iou_matrix = iou_matrix 84 | 85 | else: 86 | self.iou_matrix = np.zeros((n,m)) 87 | 88 | def greedy_match(self,iou_threshold): 89 | 90 | gt_rects_matched = [False for gt_index in range(self.m)] 91 | rects_matched = [False for r_index in range(self.n)] 92 | 93 | if self.n == 0: 94 | return [],[] 95 | elif self.m == 0: 96 | return rects_matched, [] 97 | 98 | for i,gt_index in enumerate(np.argmax(self.iou_matrix, axis=1)): 99 | if self.iou_matrix[i, gt_index] >= iou_threshold: 100 | if gt_rects_matched[gt_index] is False and rects_matched[i] is False: 101 | rects_matched[i] = True 102 | gt_rects_matched[gt_index] = True 103 | return rects_matched, gt_rects_matched 104 | 105 | 106 | def cartesian(arrays, out=None): 107 | """Generate a cartesian product of input arrays. 108 | 109 | Parameters 110 | ---------- 111 | arrays : list of array-like 112 | 1-D arrays to form the cartesian product of. 113 | out : ndarray 114 | Array to place the cartesian product in. 115 | Returns 116 | ------- 117 | out : ndarray 118 | 2-D array of shape (M, len(arrays)) containing cartesian products 119 | formed of input arrays. 120 | 121 | """ 122 | arrays = [np.asarray(x) for x in arrays] 123 | shape = (len(x) for x in arrays) 124 | dtype = arrays[0].dtype 125 | 126 | ix = np.indices(shape) 127 | ix = ix.reshape(len(arrays), -1).T 128 | 129 | if out is None: 130 | out = np.empty_like(ix, dtype=dtype) 131 | 132 | for n, arr in enumerate(arrays): 133 | out[:, n] = arrays[n][ix[:, n]] 134 | 135 | return out -------------------------------------------------------------------------------- /scoring/rectangle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Defense Innovation Unit Experimental 3 | All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | """ 17 | 18 | from __future__ import division 19 | class Rectangle(object): 20 | """Rectangle class.""" 21 | 22 | def __init__(self, xmin, ymin, xmax, ymax): 23 | """Constructs a Rectangle instance.""" 24 | if xmin >= xmax or ymin >= ymax: 25 | self.xmin_ = None 26 | self.ymin_ = None 27 | self.xmax_ = None 28 | self.ymax_ = None 29 | else: 30 | self.xmin_ = xmin 31 | self.ymin_ = ymin 32 | self.xmax_ = xmax 33 | self.ymax_ = ymax 34 | self.coords = (xmin,ymin,xmax,ymax) 35 | 36 | def __eq__(self, other): 37 | """== operator overloading.""" 38 | return ((self.xmin_ == other.xmin_) and (self.ymin_ == other.ymin_) and 39 | (self.xmax_ == other.xmax_) and (self.ymax_ == other.ymax_)) 40 | 41 | def __ne__(self, other): 42 | """!= operator overloading.""" 43 | return not self.__eq__(other) 44 | 45 | def is_empty(self): 46 | """Determines if the Rectangle instance is valid or not.""" 47 | return ((self.xmin_ is None) or (self.ymin_ is None) or 48 | (self.xmax_ is None) or (self.ymax_ is None) or 49 | (self.xmin_ >= self.xmax_) or (self.ymin_ >= self.ymax_)) 50 | 51 | def width(self): 52 | """Returns the width of the Rectangle instance.""" 53 | return self.xmax_ - self.xmin_ 54 | 55 | def height(self): 56 | """Returns the height of the Rectangle instance.""" 57 | return self.ymax_ - self.ymin_ 58 | 59 | def area(self): 60 | """Returns the area of the Rectangle instance.""" 61 | return self.width() * self.height() 62 | 63 | def intersect(self, other): 64 | """Returns the intersection of this rectangle with the other rectangle.""" 65 | 66 | xmin = max(self.xmin_, other.xmin_) 67 | ymin = max(self.ymin_, other.ymin_) 68 | xmax = min(self.xmax_, other.xmax_) 69 | ymax = min(self.ymax_, other.ymax_) 70 | return Rectangle(xmin, ymin, xmax, ymax) 71 | 72 | def intersects(self, other): 73 | """Tests if this rectangle has an intersection with another rectangle.""" 74 | return not (self.is_empty() or other.is_empty() or 75 | (other.xmax_ <= self.xmin_) or (self.xmax_ <= other.xmin_) or 76 | (other.ymax_ <= self.ymin_) or (self.ymax_ <= other.ymin_)) 77 | 78 | def contains(self, x, y): 79 | """Tests if a point is inside or on any of the edges of the rectangle.""" 80 | return ((x >= self.xmin_) and (x <= self.xmax_) and 81 | (y >= self.ymin_) and (y <= self.ymax_)) 82 | 83 | def intersect_over_union(self, other): 84 | """Returns the intersection over union ratio of this and other rectangle.""" 85 | if not self.intersects(other): 86 | return 0.0 87 | 88 | intersect_rect = self.intersect(other) 89 | if intersect_rect.is_empty(): 90 | return 0.0 91 | 92 | if self.area() == 0 or other.area() == 0: 93 | return 0.0 94 | 95 | return intersect_rect.area() / (self.area() + other.area() - intersect_rect.area()) 96 | -------------------------------------------------------------------------------- /scoring/requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | numpy 3 | tqdm 4 | -------------------------------------------------------------------------------- /scoring/run_scoring.sh: -------------------------------------------------------------------------------- 1 | python score.py /home/ubuntu/anyan/harvey_data/data_utilities/inference/preds_output_noclean_2class_v2/ /home/ubuntu/anyan/harvey_data/harvey_test_second_noblack_ms_noclean.geojson 2 | 3 | -------------------------------------------------------------------------------- /scoring/score.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -exuo pipefail 4 | 5 | export input_repo=$(ls /pfs -1 | grep -v out | grep -v labels) 6 | 7 | export groundtruth=$(ls /pfs/validate_labels -1) 8 | groundtruth="/pfs/validate_labels/$groundtruth" 9 | 10 | export userID=`echo $input_repo | cut -f 1 -d "_"` 11 | echo "Scoring userID=$userID" 12 | 13 | timestamp=`date +%F:%T` 14 | mkdir -p /pfs/out/$timestamp 15 | 16 | # Note ... score.py needs the trailing slash on the input path 17 | python score.py /pfs/$input_repo/ $groundtruth --output /pfs/out/$timestamp 18 | 19 | -------------------------------------------------------------------------------- /scoring/score.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Defense Innovation Unit Experimental 3 | All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Modifications copyright (C) 2018 18 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 19 | Written by An Yan 20 | """ 21 | 22 | from __future__ import division 23 | from PIL import Image 24 | import numpy as np 25 | import json 26 | import os 27 | from tqdm import tqdm 28 | import argparse 29 | import math 30 | from matching import Matching 31 | import csv 32 | from rectangle import Rectangle 33 | import time 34 | 35 | 36 | #debug 37 | # geospatial 38 | #import data_utilities.aug_util as aug 39 | #import data_utilities.wv_util as wv 40 | ''' 41 | This script does not apply non-maximum suppression. Instead, it uses 42 | a hard score threshold i.e. 0.5 to determine the final set of detection 43 | bboxes out of detection results. 44 | 45 | For more on non-maximum suppresion: 46 | 47 | https://github.com/tensorflow/models/blob/master/research/object_detection/utils/per_image_evaluation.py 48 | 49 | line 336 50 | 51 | and 52 | 53 | https://github.com/tensorflow/models/blob/master/research/object_detection/utils/np_box_mask_list_ops.py 54 | 55 | ''' 56 | 57 | 58 | """ 59 | Scoring code to calculate per-class precision and mean average precision. 60 | 61 | Args: 62 | predictions: a folder path of prediction files. 63 | Prediction files should have filename format 'XYZ.tif.txt', 64 | where 'XYZ.tif' is the xView TIFF file being predicted on. 65 | Prediction files should be in space-delimited csv format, with each 66 | line like (xmin ymin xmax ymax class_prediction score_prediction). 67 | ie ("predictions/") 68 | 69 | groundtruth: a filepath to ground truth labels (GeoJSON format) 70 | ie ("ground_truth.geojson") 71 | 72 | output (-o): a folder path where output metrics are saved 73 | ie ("scores/") 74 | 75 | Outputs: 76 | Writes two files to the 'output' parameter folder: 'score.txt' and 'metrics.txt' 77 | 'score.txt' contains a single floating point value output: mAP 78 | 'metrics.txt' contains the remaining metrics in per-line format (metric/class_num: score_float) 79 | 80 | """ 81 | # This is for Tomnox + Microsoft building footprint data 82 | # this is for geojson with 2 classes: damaged and non-damaged 83 | # TODO: add offset 84 | def get_labels_w_uid_nondamaged(fname): 85 | """ 86 | Gets label data from a geojson label file 87 | Args: 88 | fname: file path to an xView geojson label file 89 | Output: 90 | Returns three arrays: coords, chips, and classes corresponding to the 91 | coordinates, file-names, and classes for each ground truth. 92 | """ 93 | # debug 94 | x_off = 15 95 | y_off = 15 96 | right_shift = 5 # how much shift to the right 97 | add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 98 | with open(fname) as f: 99 | data = json.load(f) 100 | 101 | coords = np.zeros((len(data['features']),4)) 102 | chips = np.zeros((len(data['features'])),dtype="object") 103 | classes = np.zeros((len(data['features']))) 104 | # debug 105 | uids = np.zeros((len(data['features']))) 106 | 107 | for i in tqdm(range(len(data['features']))): 108 | if data['features'][i]['properties']['bb'] != []: 109 | try: 110 | b_id = data['features'][i]['properties']['Joined lay'] 111 | # if b_id == '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif': 112 | # print('found chip!') 113 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 114 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 115 | 116 | chips[i] = b_id 117 | classes[i] = data['features'][i]['properties']['type'] 118 | # debug 119 | except: 120 | # print('i:', i) 121 | # print(data['features'][i]['properties']['bb']) 122 | pass 123 | if val.shape[0] != 4: 124 | print("Issues at %d!" % i) 125 | else: 126 | coords[i] = val 127 | else: 128 | chips[i] = 'None' 129 | # debug 130 | # added offsets to each coordinates 131 | # need to check the validity of bbox maybe 132 | coords = np.add(coords, add_np) 133 | 134 | return coords, chips, classes, uids 135 | 136 | 137 | 138 | 139 | 140 | 141 | def get_labels(fname): 142 | """ 143 | Processes a WorldView3 GEOJSON file 144 | 145 | Args: 146 | fname: filepath to the GeoJson file. 147 | 148 | Outputs: 149 | Bounding box coordinate array, Chip-name array, and Classes array 150 | 151 | """ 152 | with open(fname) as f: 153 | data = json.load(f) 154 | 155 | coords = np.zeros((len(data['features']),4)) 156 | chips = np.zeros((len(data['features'])),dtype="object") 157 | classes = np.zeros((len(data['features']))) 158 | 159 | for i in tqdm(range(len(data['features']))): 160 | if data['features'][i]['properties']['bounds_imcoords'] != []: 161 | b_id = data['features'][i]['properties']['image_id'] 162 | val = np.array([int(num) for num in data['features'][i]['properties']['bounds_imcoords'].split(",")]) 163 | chips[i] = b_id 164 | classes[i] = data['features'][i]['properties']['type_id'] 165 | if val.shape[0] != 4: 166 | raise ValueError('A bounding box should have 4 entries!') 167 | else: 168 | coords[i] = val 169 | else: 170 | chips[i] = 'None' 171 | 172 | return coords, chips, classes 173 | 174 | 175 | 176 | 177 | 178 | 179 | # changed to deal with harvey data 180 | # shifting bboxes by 15 181 | def get_labels_harvey(fname): 182 | """ 183 | Gets label data from a geojson label file 184 | Args: 185 | fname: file path to an xView geojson label file 186 | Output: 187 | Returns three arrays: coords, chips, and classes corresponding to the 188 | coordinates, file-names, and classes for each ground truth. 189 | """ 190 | # debug 191 | x_off = 15 192 | y_off = 15 193 | right_shift = 5 # how much shift to the right 194 | add_np = np.array([-x_off + right_shift, -y_off, x_off + right_shift, y_off]) # shift to the rihgt 195 | with open(fname) as f: 196 | data = json.load(f) 197 | 198 | coords = np.zeros((len(data['features']),4)) 199 | chips = np.zeros((len(data['features'])),dtype="object") 200 | classes = np.zeros((len(data['features']))) 201 | 202 | for i in tqdm(range(len(data['features']))): 203 | if data['features'][i]['properties']['bb'] != []: 204 | try: 205 | b_id = data['features'][i]['properties']['IMAGE_ID'] 206 | # if b_id == '20170831_105001000B95E100_3020021_jpeg_compressed_06_01.tif': 207 | # print('found chip!') 208 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 209 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 210 | 211 | ymin = val[3] 212 | ymax = val[1] 213 | val[1] = ymin 214 | val[3] = ymax 215 | chips[i] = b_id 216 | classes[i] = data['features'][i]['properties']['TYPE_ID'] 217 | except: 218 | # print('i:', i) 219 | # print(data['features'][i]['properties']['bb']) 220 | pass 221 | if val.shape[0] != 4: 222 | print("Issues at %d!" % i) 223 | else: 224 | coords[i] = val 225 | else: 226 | chips[i] = 'None' 227 | # debug 228 | # added offsets to each coordinates 229 | # need to check the validity of bbox maybe 230 | coords = np.add(coords, add_np) 231 | 232 | return coords, chips, classes 233 | 234 | 235 | 236 | 237 | # def get_labels_w_uid_nondamaged(fname), return return coords, chips, classes, uids 238 | 239 | 240 | def convert_to_rectangle_list(coordinates): 241 | """ 242 | Converts a list of coordinates to a list of rectangles 243 | 244 | Args: 245 | coordinates: a flattened list of bounding box coordinates in format 246 | (xmin,ymin,xmax,ymax) 247 | 248 | Outputs: 249 | A list of rectangles 250 | 251 | """ 252 | rectangle_list = [] 253 | number_of_rects = int(len(coordinates) / 4) 254 | for i in range(number_of_rects): 255 | rectangle_list.append(Rectangle( 256 | coordinates[4 * i], coordinates[4 * i + 1], coordinates[4 * i + 2], 257 | coordinates[4 * i + 3])) 258 | return rectangle_list 259 | 260 | def ap_from_pr(p,r): 261 | """ 262 | Calculates AP from precision and recall values as specified in 263 | the PASCAL VOC devkit. 264 | 265 | Args: 266 | p: an array of precision values 267 | r: an array of recall values 268 | 269 | Outputs: 270 | An average precision value 271 | 272 | """ 273 | r = np.concatenate([[0], r, [1]]) 274 | p = np.concatenate([[0], p, [0]]) 275 | for i in range(p.shape[0] - 2, 0, -1): 276 | if p[i] > p[i-1]: 277 | p[i-1] = p[i] 278 | 279 | i = np.where(r[1:] != r[:len(r)-1])[0] + 1 280 | ap = np.sum( 281 | (r[i] - r[i - 1]) * p[i]) 282 | 283 | return ap 284 | 285 | 286 | # added 287 | ''' 288 | remove predictions that have class = 0 from the list of predictions of an image 289 | args: 290 | prediction_list: a list of predictions: (xmin ymin xmax ymax class_prediction score_prediction) 291 | ''' 292 | def remove_invalid_predictions(prediction_list): 293 | new_list = list() 294 | for pred in prediction_list: 295 | if pred[4] == 0: 296 | continue 297 | new_list.append(pred) 298 | return new_list 299 | 300 | 301 | 302 | 303 | 304 | 305 | def score(path_predictions, path_groundtruth, path_output, iou_threshold = .5): 306 | """ 307 | Compute metrics on a number of prediction files, given a folder of prediction files 308 | and a ground truth. Primary metric is mean average precision (mAP). 309 | 310 | Args: 311 | path_predictions: a folder path of prediction files. 312 | Prediction files should have filename format 'XYZ.tif.txt', 313 | where 'XYZ.tif' is the xView TIFF file being predicted on. 314 | Prediction files should be in space-delimited csv format, with each 315 | line like (xmin ymin xmax ymax class_prediction score_prediction) 316 | 317 | path_groundtruth: a file path to a single ground truth geojson 318 | 319 | path_output: a folder path for output scoring files 320 | 321 | iou_threshold: a float between 0 and 1 indicating the percentage 322 | iou required to count a prediction as a true positive 323 | 324 | Outputs: 325 | Writes two files to the 'path_output' parameter folder: 'score.txt' and 'metrics.txt' 326 | 'score.txt' contains a single floating point value output: mAP 327 | 'metrics.txt' contains the remaining metrics in per-line format (metric/class_num: score_float) 328 | 329 | Raises: 330 | ValueError: if there are files in the prediction folder that are not in the ground truth geojson. 331 | EG a prediction file is titled '15.tif.txt', but the file '15.tif' is not in the ground truth. 332 | 333 | """ 334 | assert (iou_threshold < 1 and iou_threshold > 0) 335 | 336 | ttime = time.time() 337 | boxes_dict = {} 338 | pchips = [] 339 | stclasses = [] 340 | num_preds = 0 341 | 342 | 343 | # pchips: prediction txt 344 | for file in tqdm(os.listdir(path_predictions)): 345 | fname = file.split(".txt")[0] 346 | pchips.append(fname) 347 | # debug 348 | with open(path_predictions + file,'r') as f: 349 | 350 | #arr = np.array(list(csv.reader(f,delimiter=" "))) 351 | 352 | # maybe not needed 353 | predict_list = list(csv.reader(f,delimiter=" ")) 354 | new_list = remove_invalid_predictions(predict_list) 355 | arr = np.array(new_list) 356 | if arr.shape[0] == 0: 357 | #If the file is empty, we fill it in with an array of zeros 358 | boxes_dict[fname] = np.array([[0,0,0,0,0,0]]) 359 | num_preds += 1 360 | else: 361 | arr = arr[:,:6].astype(np.float64) 362 | # TODO: may adjust the threshold of scores that to be counted as valid predictions 363 | # default = 0 364 | # There should be a nms mode 365 | threshold = 0.4 366 | arr = arr[arr[:,5] > threshold] 367 | stclasses += list(arr[:,4]) 368 | num_preds += arr.shape[0] 369 | 370 | if np.any(arr[:,:4] < 0): 371 | raise ValueError('Bounding boxes cannot be negative.') 372 | 373 | if np.any(arr[:,5] < 0) or np.any(arr[:,5] > 1): 374 | raise ValueError('Confidence scores should be between 0 and 1.') 375 | 376 | boxes_dict[fname] = arr[:,:6] 377 | 378 | pchips = sorted(pchips) 379 | stclasses = np.unique(stclasses).astype(np.int64) 380 | 381 | 382 | # debug 383 | #gt_coords, gt_chips, gt_classes = get_labels(path_groundtruth) 384 | gt_coords, gt_chips, gt_classes, _ =get_labels_w_uid_nondamaged(path_groundtruth) 385 | 386 | # TODO: add removing bboxes over clouds manually or / test images should not contain any black chips 387 | 388 | gt_unique = np.unique(gt_classes.astype(np.int64)) 389 | #debug 390 | print('gt_unique: ', gt_unique) 391 | max_gt_cls = 100 # max number of classes 392 | # debug 393 | # need to remove class 0 from evaluation 394 | ignored_classes = [0] 395 | gt_unique_ig = np.array([i for i in gt_unique if int(i) not in ignored_classes], dtype = np.int64) 396 | 397 | 398 | #added 399 | # get statistics of ground truth 400 | num_gt_class = dict() 401 | for i in gt_unique: 402 | num_gt_class[i] = gt_classes[gt_classes==i].shape[0] 403 | 404 | 405 | if set(pchips).issubset(set(gt_unique_ig)): 406 | raise ValueError('The prediction files {%s} are not in the ground truth.' % str(set(pchips) - (set(gt_unique)))) 407 | 408 | #print("Number of Predictions: %d" % num_preds) 409 | #print("Number of GT: %d" % np.sum(gt_classes.shape) ) 410 | 411 | 412 | per_file_class_data = {} 413 | for i in gt_unique_ig: 414 | per_file_class_data[i] = [[],[]] 415 | 416 | num_gt_per_cls = np.zeros((max_gt_cls)) 417 | 418 | for file_ind in range(len(pchips)): 419 | print(pchips[file_ind]) 420 | det_box = boxes_dict[pchips[file_ind]][:,:4] 421 | det_scores = boxes_dict[pchips[file_ind]][:,5] 422 | det_cls = boxes_dict[pchips[file_ind]][:,4] 423 | 424 | gt_box = gt_coords[(gt_chips==pchips[file_ind]).flatten()] 425 | gt_cls = gt_classes[(gt_chips==pchips[file_ind])] 426 | 427 | for i in gt_unique: 428 | s = det_scores[det_cls == i] 429 | ssort = np.argsort(s)[::-1] 430 | per_file_class_data[i][0] += s[ssort].tolist() 431 | 432 | gt_box_i_cls = gt_box[gt_cls == i].flatten().tolist() 433 | det_box_i_cls = det_box[det_cls == i] 434 | det_box_i_cls = det_box_i_cls[ssort].flatten().tolist() 435 | 436 | gt_rects = convert_to_rectangle_list(gt_box_i_cls) 437 | rects = convert_to_rectangle_list(det_box_i_cls) 438 | 439 | matching = Matching(gt_rects, rects) 440 | rects_matched, gt_matched = matching.greedy_match(iou_threshold) 441 | # debug 442 | print('len(gt_matched): ', len(gt_matched)) 443 | print('len(rects_matched): ', len(rects_matched)) 444 | #print('rects_matched: ', rects_matched) 445 | 446 | 447 | #we aggregate confidence scores, rectangles, and num_gt across classes 448 | #per_file_class_data[i][0] += det_scores[det_cls == i].tolist() 449 | per_file_class_data[i][1] += rects_matched 450 | num_gt_per_cls[i] += len(gt_matched) 451 | 452 | average_precision_per_class = np.ones(max_gt_cls) * float('nan') 453 | per_class_p = np.ones(max_gt_cls) * float('nan') 454 | per_class_r = np.ones(max_gt_cls) * float('nan') 455 | 456 | # debug 457 | # need to remove class 0 from evaluation 458 | ignored_classes = [0] 459 | gt_unique_ig = np.array([i for i in gt_unique if int(i) not in ignored_classes], dtype = np.int64) 460 | 461 | 462 | 463 | for i in gt_unique_ig: 464 | scores = np.array(per_file_class_data[i][0]) 465 | rects_matched = np.array(per_file_class_data[i][1]) 466 | 467 | if num_gt_per_cls[i] != 0: 468 | sorted_indices = np.argsort(scores)[::-1] 469 | tp_sum = np.cumsum(rects_matched[sorted_indices]) 470 | fp_sum = np.cumsum(np.logical_not(rects_matched[sorted_indices])) 471 | # calculated using confidence scores of the bboxes that have confidence score > 0.5 (or some other threshold) 472 | precision = tp_sum / (tp_sum + fp_sum + np.spacing(1)) 473 | recall = tp_sum / num_gt_per_cls[i] 474 | # debug 475 | # per_class_precision: @IOU >= 0.5, # of correctly identified bboxes / all predicted boxes 476 | per_class_p[i] = np.sum(rects_matched) / len(rects_matched) 477 | per_class_r[i] = np.sum(rects_matched) / num_gt_per_cls[i] 478 | ap = ap_from_pr(precision,recall) 479 | 480 | # added 481 | print('for class: ', i) 482 | print('TP: ', tp_sum[-1]) 483 | print('FP: ', fp_sum[-1]) 484 | 485 | else: 486 | ap = float('nan') 487 | average_precision_per_class[i] = ap 488 | 489 | # debug 490 | #metric splits 491 | #metric_keys = ['map','map/small','map/medium','map/large', 492 | #'map/common','map/rare'] 493 | 494 | metric_keys = ['map'] 495 | 496 | ''' 497 | splits = { 498 | 'map/small': [17, 18, 19, 20, 21, 23, 24, 26, 27, 28, 32, 41, 60, 499 | 62, 63, 64, 65, 66, 91], 500 | 'map/medium': [11, 12, 15, 25, 29, 33, 34, 35, 36, 37, 38, 42, 44, 501 | 47, 50, 53, 56, 59, 61, 71, 72, 73, 76, 84, 86, 93, 94], 502 | 'map/large': [13, 40, 45, 49, 51, 52, 54, 55, 57, 74, 77, 79, 83, 89], 503 | 504 | 'map/common': [13,17,18,19,20,21,23,24,25,26,27,28,34,35,41, 505 | 47,60,63,64,71,72,73,76,77,79,83,86,89,91], 506 | 'map/rare': [11,12,15,29,32,33,36,37,38,40,42,44,45,49,50, 507 | 51,52,53,54,55,56,57,59,61,62,65,66,74,84,93,94] 508 | } 509 | ''' 510 | vals = {} 511 | vals['map'] = np.nanmean(average_precision_per_class) 512 | vals['map_score'] = np.nanmean(per_class_p) 513 | vals['mar_score'] = np.nanmean(per_class_r) 514 | 515 | 516 | ''' 517 | for i in splits.keys(): 518 | vals[i] = np.nanmean(average_precision_per_class[splits[i]]) 519 | ''' 520 | 521 | 522 | for i in gt_unique: 523 | vals[int(i)] = average_precision_per_class[int(i)] 524 | 525 | vals['f1'] = 2 / ( (1 / (np.spacing(1) + vals['map_score']) ) 526 | + ( 1 / ( np.spacing(1) + vals['mar_score'])) ) 527 | 528 | #print("mAP: %f | mAP score: %f | mAR: %f | F1: %f" % 529 | print("mAP: %f | mean precision: %f | mean recall: %f | F1: %f" % 530 | (vals['map'],vals['map_score'],vals['mar_score'],vals['f1'])) 531 | 532 | with open(path_output + '/score.txt','w') as f: 533 | f.write(str("%.8f" % vals['map'])) 534 | 535 | with open(path_output + '/metrics.txt','w') as f: 536 | for key in vals.keys(): 537 | f.write("%s %f\n" % (str(key),vals[key]) ) 538 | 539 | 540 | # added 541 | print('counting score threshold larger than %s as valid prediction' % str(threshold)) 542 | for k, v in num_gt_class.items(): 543 | print('ground truth class: ', k) 544 | print('the count of GT labels: ', v) 545 | 546 | print("Number of Predictions: %d" % num_preds) 547 | print("Number of GT: %d" % np.sum(gt_classes.shape) ) 548 | 549 | 550 | 551 | print("Final time: %s" % str(time.time() - ttime)) 552 | 553 | if __name__ == "__main__": 554 | parser = argparse.ArgumentParser() 555 | parser.add_argument("predictions", help="Path to properly formatted predictions file") 556 | parser.add_argument("groundtruth", help="Path to groundtruth GeoJSON file") 557 | parser.add_argument('-o',"--output",default=".", 558 | help="Output path for calculated scores") 559 | args = parser.parse_args() 560 | 561 | score(args.predictions, args.groundtruth, args.output) 562 | 563 | -------------------------------------------------------------------------------- /selective_copy.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | 6 | ''' 7 | 8 | # select image chips that are with digital globe labels 9 | 10 | 11 | 12 | import wv_util as wv 13 | import matplotlib.pyplot as plt 14 | import numpy as np 15 | import csv 16 | #%matplotlib inline 17 | #import matplotlib, copy, skimage, os, tifffile 18 | from skimage import io, morphology, draw 19 | import gdal 20 | from PIL import Image 21 | import random 22 | import json 23 | from tqdm import tqdm 24 | import io 25 | import glob 26 | import shutil 27 | import os 28 | 29 | 30 | 31 | def get_unique_images(fname): 32 | with open(fname) as f: 33 | data = json.load(f) 34 | 35 | coords = np.zeros((len(data['features']),4)) 36 | chips = np.zeros((len(data['features'])),dtype="object") 37 | classes = np.zeros((len(data['features']))) 38 | 39 | for i in tqdm(range(len(data['features']))): 40 | if data['features'][i]['properties']['bb'] != []: 41 | try: 42 | b_id = data['features'][i]['properties']['Joined lay'] 43 | if b_id == '20170902_10400100324DAE00_3210111_jpeg_compressed_09_05.tif': 44 | print('found chip!') 45 | bbox = data['features'][i]['properties']['bb'][1:-1].split(",") 46 | 47 | val = np.array([int(num) for num in data['features'][i]['properties']['bb'][1:-1].split(",")]) 48 | 49 | ymin = val[3] 50 | ymax = val[1] 51 | val[1] = ymin 52 | val[3] = ymax 53 | #print(val) 54 | chips[i] = str(b_id) 55 | 56 | classes[i] = data['features'][i]['properties']['type'] 57 | except: 58 | print('i:', i) 59 | print(data['features'][i]['properties']['Joined lay']) 60 | #pass 61 | if val.shape[0] != 4: 62 | print("Issues at %d!" % i) 63 | else: 64 | coords[i] = val 65 | else: 66 | chips[i] = 'None' 67 | print('warning: chip is none') 68 | unique_image_set = set(chips.tolist()) 69 | return unique_image_set 70 | 71 | 72 | 73 | 74 | def main(): 75 | 76 | geojson_file = '../harvey_test_second_noblack_ms_noclean.geojson' 77 | unique_image_set = get_unique_images(geojson_file) 78 | print('number of chips is :', len(unique_image_set)) 79 | 80 | path = '/home/ubuntu/anyan/harvey_data/harvey_test_second_noblack/' 81 | save_path = '/home/ubuntu/anyan/harvey_data/harvey_test_bigtiff_v3/' 82 | 83 | 84 | files = [os.path.join(path, f) for f in os.listdir(path)] 85 | 86 | i = 0 87 | #parent_folder = os.path.abspath(abs_dirname + "/../") 88 | #seperate_subdir = None 89 | #subdir_name = os.path.join(parent_folder, 'train_small') 90 | #seperate_subdir = subdir_name 91 | #os.mkdir(subdir_name) 92 | 93 | for f in files: 94 | # create new subdir if necessary 95 | 96 | #subdir_name = os.path.join(abs_dirname, '{0:03d}'.format(i / N + 1)) 97 | # os.mkdir(subdir_name) 98 | # seperate_subdir = subdir_name 99 | 100 | # copy file to current dir 101 | f_base = os.path.basename(f) 102 | if f_base in unique_image_set: 103 | print('filename: ', f_base) 104 | shutil.copy(f, os.path.join(save_path, f_base)) 105 | i += 1 106 | print('copied: ', i) 107 | 108 | 109 | 110 | if __name__ == '__main__': 111 | main() 112 | -------------------------------------------------------------------------------- /split_geojson.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | ''' 6 | 7 | ''' 8 | Split a geojson file which contains labels for bounding boxes to training 9 | and test geojson files according to the files in training and test folder respectively 10 | ''' 11 | 12 | import argparse 13 | import os 14 | import geopandas as gpd 15 | import shapely.geometry 16 | import shutil 17 | 18 | 19 | 20 | 21 | 22 | def get_filenames(abs_dirname): 23 | 24 | files = [os.path.join(abs_dirname, f) for f in os.listdir(abs_dirname)] 25 | 26 | i = 0 27 | # parent_folder = os.path.abspath(abs_dirname + "/../") 28 | # seperate_subdir = None 29 | #subdir_name = os.path.join(parent_folder, 'train_small') 30 | #seperate_subdir = subdir_name 31 | #os.mkdir(subdir_name) 32 | name_list = [] 33 | for f in files: 34 | filename_origin = str(f) 35 | filename = filename_origin.split('/')[-1] 36 | #print filename 37 | name_list.append(filename) 38 | # create new subdir if necessary 39 | # if i < N: 40 | #subdir_name = os.path.join(abs_dirname, '{0:03d}'.format(i / N + 1)) 41 | # os.mkdir(subdir_name) 42 | # seperate_subdir = subdir_name 43 | 44 | # copy file to current dir 45 | #f_base = os.path.basename(f) 46 | #shutil.copy(f, os.path.join(subdir_name, f_base)) 47 | i += 1 48 | return name_list 49 | 50 | 51 | 52 | 53 | 54 | 55 | # for Tomnod + MS data 56 | def geojson_split_multiclass(geojson_ori, src_dir, suffix): 57 | name_list = set(get_filenames(os.path.abspath(src_dir))) 58 | gfN = gpd.read_file(geojson_ori) 59 | index_list = [] 60 | df_len = len(gfN) 61 | 62 | for i in range(0, df_len): 63 | print('idx', i) 64 | series_tmp = gfN.loc[i] 65 | if series_tmp['Joined lay'] in name_list: 66 | index_list.append(i) 67 | geometries = [xy for xy in list(gfN.iloc[index_list]['geometry'])] 68 | crs = {'init': 'epsg:4326'} 69 | gf = gpd.GeoDataFrame(gfN.iloc[index_list], crs=crs, geometry=geometries) 70 | 71 | # geometries = [shapely.geometry.Point(xy) for xy in zip(df.lng, df.lat)] 72 | # gf = gpd.GeoDataFrame(gfN.iloc[0],) 73 | parent_folder = os.path.abspath(geojson_ori + "/../") 74 | 75 | # get training or test dir name 76 | f_base = os.path.basename(src_dir) 77 | save_name = f_base + '_'+suffix+ '.geojson' 78 | print('saving file: ', save_name) 79 | #path = os.path.join(subdir_name, f_base) 80 | gf.to_file(parent_folder+'/'+ save_name, driver='GeoJSON') 81 | 82 | 83 | 84 | # for Tomnod + Oak Ridge building footprint 85 | def geojson_split(geojson_ori, src_dir, suffix): 86 | name_list = set(get_filenames(os.path.abspath(src_dir))) 87 | gfN = gpd.read_file(geojson_ori) 88 | index_list = [] 89 | df_len = len(gfN) 90 | 91 | for i in range(0, df_len): 92 | print('idx', i) 93 | series_tmp = gfN.loc[i] 94 | if series_tmp['IMAGE_ID'] in name_list: 95 | index_list.append(i) 96 | geometries = [xy for xy in list(gfN.iloc[index_list]['geometry'])] 97 | crs = {'init': 'epsg:4326'} 98 | gf = gpd.GeoDataFrame(gfN.iloc[index_list], crs=crs, geometry=geometries) 99 | 100 | # geometries = [shapely.geometry.Point(xy) for xy in zip(df.lng, df.lat)] 101 | # gf = gpd.GeoDataFrame(gfN.iloc[0],) 102 | parent_folder = os.path.abspath(geojson_ori + "/../") 103 | 104 | # get training or test dir name 105 | f_base = os.path.basename(src_dir) 106 | save_name = f_base + '_'+suffix+ '.geojson' 107 | print('saving file: ', save_name) 108 | #path = os.path.join(subdir_name, f_base) 109 | gf.to_file(parent_folder+'/'+ save_name, driver='GeoJSON') 110 | 111 | 112 | 113 | 114 | def parse_args(): 115 | """Parse command line arguments passed to script invocation.""" 116 | parser = argparse.ArgumentParser( 117 | description='Split files into multiple subfolders.') 118 | 119 | parser.add_argument('train_dir', help='directory containing training files') 120 | parser.add_argument('test_dir', help='directory containing test files') 121 | parser.add_argument('src_geojson', help='source geojson') 122 | parser.add_argument("-s", "--suffix", type=str, default='v1', 123 | help="Output geojson suffix. Default suffix 'v1' will output 'harvey_train_second_v1.geojson'") 124 | return parser.parse_args() 125 | 126 | 127 | def main(): 128 | """Module's main entry point (zopectl.command).""" 129 | args = parse_args() 130 | train_dir = args.train_dir 131 | test_dir = args.test_dir 132 | geojson_ori = args.src_geojson 133 | suffix = args.suffix 134 | ''' 135 | if not os.path.exists(src_dir): 136 | raise Exception('Directory does not exist ({0}).'.format(src_dir)) 137 | ''' 138 | #get_filenames(os.path.abspath(src_dir)) 139 | geojson_split_multiclass(os.path.abspath(geojson_ori),os.path.abspath(train_dir), suffix) 140 | geojson_split_multiclass(os.path.abspath(geojson_ori),os.path.abspath(test_dir),suffix) 141 | #move_files(os.path.abspath(src_dir)) 142 | #seperate_nfiles(os.path.abspath(src_dir)) 143 | 144 | if __name__ == '__main__': 145 | main() 146 | -------------------------------------------------------------------------------- /tfr_util.py: -------------------------------------------------------------------------------- 1 | # Original work Copyright 2017 The TensorFlow Authors. All Rights Reserved. 2 | # Modifications Copyright 2018 Defense Innovation Unit Experimental. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # ============================================================================== 16 | 17 | 18 | ''' 19 | Modifications copyright (C) 2018 20 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 21 | Written by An Yan 22 | ''' 23 | 24 | from PIL import Image 25 | import tensorflow as tf 26 | import io 27 | import numpy as np 28 | 29 | ''' 30 | TensorflowRecord (TFRecord) processing helper functions to be re-used by any scripts 31 | that create or read TFRecord files. 32 | ''' 33 | 34 | def to_tf_example(img, boxes, class_num): 35 | """ 36 | Converts a single image with respective boxes into a TFExample. Multiple TFExamples make up a TFRecord. 37 | 38 | Args: 39 | img: an image array 40 | boxes: an array of bounding boxes for the given image 41 | class_num: an array of class numbers for each bouding box 42 | 43 | Output: 44 | A TFExample containing encoded image data, scaled bounding boxes with classes, and other metadata. 45 | """ 46 | encoded = convertToJpeg(img) 47 | 48 | width = img.shape[0] 49 | height = img.shape[1] 50 | 51 | xmin = [] 52 | ymin = [] 53 | xmax = [] 54 | ymax = [] 55 | classes = [] 56 | classes_text = [] 57 | 58 | for ind,box in enumerate(boxes): 59 | xmin.append(box[0] / width) 60 | ymin.append(box[1] / height) 61 | xmax.append(box[2] / width) 62 | ymax.append(box[3] / height) 63 | classes.append(int(class_num[ind])) 64 | 65 | example = tf.train.Example(features=tf.train.Features(feature={ 66 | 'image/height': int64_feature(height), 67 | 'image/width': int64_feature(width), 68 | 'image/encoded': bytes_feature(encoded), 69 | 'image/format': bytes_feature('jpeg'.encode('utf8')), 70 | 'image/object/bbox/xmin': float_list_feature(xmin), 71 | 'image/object/bbox/xmax': float_list_feature(xmax), 72 | 'image/object/bbox/ymin': float_list_feature(ymin), 73 | 'image/object/bbox/ymax': float_list_feature(ymax), 74 | 'image/object/class/label': int64_list_feature(classes), 75 | })) 76 | 77 | return example 78 | 79 | def convertToJpeg(im): 80 | """ 81 | Converts an image array into an encoded JPEG string. 82 | 83 | Args: 84 | im: an image array 85 | 86 | Output: 87 | an encoded byte string containing the converted JPEG image. 88 | """ 89 | with io.BytesIO() as f: 90 | im = Image.fromarray(im) 91 | im.save(f, format='JPEG') 92 | return f.getvalue() 93 | 94 | def create_tf_record(output_filename, images, boxes): 95 | """ DEPRECIATED 96 | Creates a TFRecord file from examples. 97 | 98 | Args: 99 | output_filename: Path to where output file is saved. 100 | images: an array of images to create a record for 101 | boxes: an array of bounding box coordinates ([xmin,ymin,xmax,ymax]) with the same index as images 102 | """ 103 | writer = tf.python_io.TFRecordWriter(output_filename) 104 | k = 0 105 | for idx, image in enumerate(images): 106 | if idx % 100 == 0: 107 | print('On image %d of %d' %(idx, len(images))) 108 | 109 | tf_example = to_tf_example(image,boxes[idx],fname) 110 | if np.array(tf_example.features.feature['image/object/bbox/xmin'].float_list.value[0]).any(): 111 | writer.write(tf_example.SerializeToString()) 112 | k = k + 1 113 | 114 | print("saved: %d chips" % k) 115 | writer.close() 116 | 117 | ## VARIOUS HELPERS BELOW ## 118 | 119 | def int64_feature(value): 120 | return tf.train.Feature(int64_list=tf.train.Int64List(value=[value])) 121 | 122 | 123 | def int64_list_feature(value): 124 | return tf.train.Feature(int64_list=tf.train.Int64List(value=value)) 125 | 126 | 127 | def bytes_feature(value): 128 | return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value])) 129 | 130 | 131 | def bytes_list_feature(value): 132 | return tf.train.Feature(bytes_list=tf.train.BytesList(value=value)) 133 | 134 | 135 | def float_list_feature(value): 136 | return tf.train.Feature(float_list=tf.train.FloatList(value=value)) 137 | -------------------------------------------------------------------------------- /tomnod_vis/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/.DS_Store -------------------------------------------------------------------------------- /tomnod_vis/blue-tarp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/blue-tarp.jpg -------------------------------------------------------------------------------- /tomnod_vis/flooded.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/flooded.jpg -------------------------------------------------------------------------------- /tomnod_vis/flooded2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/flooded2.jpg -------------------------------------------------------------------------------- /tomnod_vis/flooded3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/flooded3.jpg -------------------------------------------------------------------------------- /tomnod_vis/flooded_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/flooded_large.png -------------------------------------------------------------------------------- /tomnod_vis/roof-damage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/annieyan/PreprocessSatelliteImagery-ObjectDetection/810858fea903ca9deaa587f379649daff1907635/tomnod_vis/roof-damage.jpg -------------------------------------------------------------------------------- /train_test_split.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (C) 2018 3 | Licensed under CC BY-NC-ND 4.0 License [see LICENSE-CC BY-NC-ND 4.0.markdown for details] 4 | Written by An Yan 5 | ''' 6 | 7 | ''' 8 | Split train : test = 8:2 9 | create two folders: harvey_train_(round_num) and harvey_test_(round_num) 10 | create two geojson files respectively for train and test 11 | ''' 12 | 13 | import argparse 14 | import os 15 | import shutil 16 | import numpy as np 17 | import itertools 18 | from random import shuffle 19 | ''' 20 | args: 21 | abs_dirname: asolute path to all chips 22 | traindir: directory name for training data. ie, harvey_train_first 23 | testdir: directory name for test data. 24 | split_percent: 0.8 for 80% training data, 20% test data 25 | ''' 26 | def seperate_nfiles(abs_dirname, traindir, testdir, split_percent): 27 | 28 | files = [os.path.join(abs_dirname, f) for f in os.listdir(abs_dirname)] 29 | 30 | i = 0 31 | parent_folder = os.path.abspath(abs_dirname + "/../") 32 | traindir_name = os.path.join(parent_folder, traindir) 33 | testdir_name = os.path.join(parent_folder, testdir) 34 | os.mkdir(traindir_name) 35 | os.mkdir(testdir_name) 36 | print('traindir_name', traindir_name) 37 | 38 | # shuffle files 39 | #file_count = len(files) 40 | #perm = np.random.permutation(file_count) 41 | shuffle(files) 42 | split_ind = int(split_percent * len(files)) 43 | num_train = 0 44 | num_test = 0 45 | 46 | for idx, f in enumerate(files): 47 | # create new subdir if necessary 48 | print('idx', idx) 49 | if idx < split_ind: 50 | #subdir_name = os.path.join(abs_dirname, '{0:03d}'.format(i / N + 1)) 51 | # os.mkdir(subdir_name) 52 | # seperate_subdir = subdir_name 53 | 54 | # copy file to current dir 55 | f_base = os.path.basename(f) 56 | shutil.copy(f, os.path.join(traindir_name, f_base)) 57 | num_train += 1 58 | else: 59 | # go to test dir 60 | f_base = os.path.basename(f) 61 | shutil.copy(f, os.path.join(testdir_name, f_base)) 62 | num_test +=1 63 | print('created training images: ', num_train) 64 | print('created test images: ', num_test) 65 | 66 | 67 | def parse_args(): 68 | """Parse command line arguments passed to script invocation.""" 69 | parser = argparse.ArgumentParser( 70 | description='Split files into multiple subfolders.') 71 | 72 | parser.add_argument('src_dir', help='source directory') 73 | 74 | return parser.parse_args() 75 | 76 | 77 | def main(): 78 | """Module's main entry point (zopectl.command).""" 79 | args = parse_args() 80 | src_dir = args.src_dir 81 | 82 | if not os.path.exists(src_dir): 83 | raise Exception('Directory does not exist ({0}).'.format(src_dir)) 84 | 85 | #move_files(os.path.abspath(src_dir)) 86 | #train_dir = 'harvey_train_train_bigtiff_v3' 87 | #test_dir = 'harvey_train_val_bigtiff_v3' 88 | train_dir = 'noaa_train_train' 89 | test_dir = 'noaa_train_val' 90 | 91 | 92 | seperate_nfiles(os.path.abspath(src_dir), train_dir, test_dir, 0.8) 93 | 94 | 95 | if __name__ == '__main__': 96 | main() 97 | 98 | --------------------------------------------------------------------------------