├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── CI.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches ├── us-county-2018.fgb └── versus-gdal.rs ├── proptest-regressions └── proptests.txt └── src ├── lib.rs ├── line.rs ├── poly.rs ├── proptests.rs └── tests ├── mod.rs └── utils.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.fgb filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | # https://matklad.github.io/2021/09/04/fast-rust-builds.html 12 | # indicates that non-incremental builds improve CI performance. 13 | CARGO_INCREMENTAL: 0 14 | # Since we're not shipping any build artifacts, turning off debug 15 | # symbols will speed things up without hurting anything. 16 | RUSTFLAGS: '-C debuginfo=0' 17 | 18 | jobs: 19 | build: 20 | name: Build and test 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: add gdal repo 25 | run: sudo apt-add-repository ppa:ubuntugis/ppa 26 | - name: and update... 27 | run: sudo apt-get update 28 | - name: install gdal (sigh) 29 | run: sudo apt install -y libgdal-dev gdal-bin 30 | 31 | - uses: actions/checkout@v2 32 | - uses: actions-rs/clippy-check@v1 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | args: --all-features 36 | 37 | # tarpaulin can only handle doctests on nightly, so let's run them 38 | # here. 39 | - name: Run doc tests 40 | run: cargo test --doc 41 | 42 | - name: Run cargo-tarpaulin 43 | uses: actions-rs/tarpaulin@v0.1 44 | with: 45 | version: '0.18.5' 46 | env: 47 | PROPTEST_CASES: 5000 48 | 49 | - name: Upload to codecov.io 50 | uses: codecov/codecov-action@v1.0.2 51 | with: 52 | token: ${{secrets.CODECOV_TOKEN}} 53 | 54 | - name: Archive code coverage results 55 | uses: actions/upload-artifact@v1 56 | with: 57 | name: code-coverage-report 58 | path: cobertura.xml 59 | 60 | release: 61 | name: Release 62 | runs-on: ubuntu-latest 63 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 64 | needs: [ build ] 65 | steps: 66 | - name: install cargo release 67 | uses: actions-rs/install@v0.1 68 | with: 69 | crate: cargo-release 70 | version: latest 71 | use-tool-cache: true 72 | - uses: actions/checkout@v2 73 | - run: git config user.name "GitHub actions" 74 | - run: git config user.email "github-actions@users.noreply.github.com" 75 | - name: cargo release 76 | run: cargo release --execute --no-confirm --token ${{secrets.CRATES_IO_TOKEN}} 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geo-rasterize" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Michael Salib "] 6 | rust-version = "1.56" # only because we're using the 2021 edition 7 | description = "a pure-rust 2D rasterizer for geospatial applications" 8 | repository = "https://github.com/msalib/geo-rasterize/" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["geospatial", "gis", "geo", "raster", "geographic"] 11 | categories = ["encoding", "graphics", "rendering", "science"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | euclid = "0.22.6" 17 | ndarray = "0.15.3" 18 | geo = "0.18.0" 19 | thiserror = "1.0.26" 20 | num-traits = "0.2.14" 21 | itertools = "0.10.1" 22 | 23 | [dev-dependencies] 24 | gdal = {version = "0.11.0", features = ["array"]} 25 | pretty_assertions = "1.0.0" 26 | anyhow = "1.0.42" 27 | proptest = "1.0.0" 28 | criterion = "0.3" 29 | flatgeobuf = "0.6.2" # can't actually build this without the default http feature 30 | geozero = {version = "0.7.7", default-features = false, features = ["with-geo"]} 31 | rand = "0.8.4" 32 | 33 | [[bench]] 34 | name = "versus-gdal" 35 | harness = false 36 | 37 | 38 | [profile.bench] 39 | debug = 1 40 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The GeoRust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geo-rasterize: a pure-rust 2D rasterizer for geospatial applications 2 | 3 | [![Crates.io][crates-badge]][crates-url] 4 | [![Docs.rs][docs-badge]][docs-url] 5 | [![CodeCov.io][codecov-badge]][codecov-url] 6 | [![Build Status][actions-badge]][actions-url] 7 | [![Python wrapper][py-badge]][py-pkg] 8 | 9 | [crates-badge]: https://img.shields.io/crates/v/geo-rasterize.svg 10 | [crates-url]: https://crates.io/crates/geo-rasterize 11 | [docs-badge]: https://img.shields.io/docsrs/geo-rasterize 12 | [docs-url]: https://docs.rs/geo-rasterize/latest/geo_rasterize/ 13 | [codecov-badge]: https://img.shields.io/codecov/c/github/msalib/geo-rasterize 14 | [codecov-url]: https://app.codecov.io/gh/msalib/geo-rasterize/ 15 | [actions-badge]: https://github.com/msalib/geo-rasterize/actions/workflows/CI.yml/badge.svg 16 | [actions-url]: https://github.com/msalib/geo-rasterize/actions?query=CI+branch%3Amain 17 | [py-badge]: https://img.shields.io/pypi/v/geo-rasterize?style=plastic 18 | [py-pkg]: https://pypi.org/project/geo-rasterize/ 19 | 20 | This crate is intended for folks who have some vector data (like a 21 | `geo::Polygon`) and a raster source (like a GeoTiff perhaps opened 22 | with `GDAL`) and who want to generate a boolean array representing 23 | which bits of raster are filled in by the polygon. There's also a 24 | [Python wrapper][py-pkg] available. 25 | 26 | This implementation is based on `GDAL`'s `GDALRasterizeGeometries` and 27 | allows you to rasterize any type supported by the `geo-types` package, 28 | including: 29 | * [Point](geo::Point) 30 | * [Line](geo::Line) 31 | * [LineString](geo::LineString) 32 | * [Polygon](geo::Polygon) 33 | * [Rect](geo::Rect) and [Triangle](geo::Triangle) 34 | * [MultiPoint](geo::MultiPoint), [MultiLineString](geo::MultiLineString), and [MultiPolygon](geo::MultiPolygon) 35 | * [Geometry](geo::Geometry) and [GeometryCollection](geo::GeometryCollection) 36 | 37 | Those shapes can have any coordintes with any numeric type as long as 38 | it can be converted into `f64`. 39 | 40 | This crate matches GDAL's behavior when GDAL is supplied with the 41 | `ALL_TOUCHED=TRUE` option. So you can use it as a drop-in replacement 42 | for GDAL if you only need a GDAL-compatible rasterizer. Also, there's 43 | no support for GDAL's `BURN_VALUE_FROM=Z`. But otherwise, this code 44 | should produce identical results to GDAL's rasterizer -- the 45 | rasterization algorithm is a direct port. We use 46 | [proptest](https://crates.io/crates/proptest) to perform randomized 47 | differential comparisons with GDAL in order bolster confidence about 48 | our conformance. 49 | 50 | 51 | 52 | 55 | 56 | 57 | ## Motivation: satellite imagery data analysis 58 | 59 | Let's say you're interested in the free 10m resolution data from ESA's 60 | [Sentinel-2](https://www.esa.int/Applications/Observing_the_Earth/Copernicus/Sentinel-2) 61 | satellite mission. You might be especially interested in how farms 62 | change over time, and you've got a bunch of farms represented as 63 | polygons. You fetch some Sentinel-2 data from 64 | [AWS](https://registry.opendata.aws/sentinel-2-l2a-cogs/). Since 65 | Sentinel-2 tiles are so large (over 110 million pixels!) and since 66 | they're stored as [Cloud Optimized GeoTiffs](https://www.cogeo.org/), 67 | you convert your polygons into windows, and extract those windows from 68 | the image tiles; that way you only have to download the pixels that 69 | you care about. 70 | 71 | But now you have a problem! Your polygons are not perfect rectangles 72 | axis-aligned to the Sentinel-2 tiling system. So while you have small 73 | field chips, you don't know which parts of those chips correspond to 74 | your polygons. Even worse, some of your polygons have holes (for 75 | example, to represent houses or ponds on the farms). That's where a 76 | [geo-rasterize] comes in! Using [geo-rasterize], you can convert your 77 | field polygons into a binary raster just like your Sentinel-2 field 78 | chips. And you can use those mask chips to select which pixels of the 79 | Sentinel-2 chips you care about. Filtering on the masks, you can now 80 | generate time series for imagery, secure in the knowledge that you're 81 | only examining pixels within your polygons! 82 | 83 | ## Binary rasterization 84 | 85 | Let's say you want to rasterize a polygon into a grid 4 pixels wide by 86 | 5 pixels high. To that, you simply construct a [BinaryRasterizer] 87 | using [BinaryBuilder], call [rasterize](BinaryRasterizer::rasterize) 88 | with your polygon and call [finish](BinaryRasterizer::finish) to get 89 | an [Array2](ndarray::Array2) of booleans. 90 | 91 | ```rust 92 | # fn main() -> geo_rasterize::Result<()> { 93 | use geo::polygon; 94 | use ndarray::array; 95 | use geo_rasterize::BinaryBuilder; 96 | 97 | let poly = polygon![ 98 | (x:4, y:2), 99 | (x:2, y:0), 100 | (x:0, y:2), 101 | (x:2, y:4), 102 | (x:4, y:2), 103 | ]; 104 | 105 | let mut r = BinaryBuilder::new().width(4).height(5).build()?; 106 | r.rasterize(&poly)?; 107 | let pixels = r.finish(); 108 | 109 | assert_eq!( 110 | pixels.mapv(|v| v as u8), 111 | array![ 112 | [0, 1, 1, 0], 113 | [1, 1, 1, 1], 114 | [1, 1, 1, 1], 115 | [0, 1, 1, 1], 116 | [0, 0, 1, 0] 117 | ] 118 | ); 119 | # Ok(()) } 120 | 121 | ``` 122 | 123 | ## ...with multiple shapes 124 | But what if you want to rasterize several geometries? That's easy enough! 125 | 126 | ```rust 127 | # fn main() -> geo_rasterize::Result<()> { 128 | use geo::{Geometry, Line, Point}; 129 | use ndarray::array; 130 | use geo_rasterize::BinaryBuilder; 131 | 132 | let shapes: Vec> = 133 | vec![Point::new(3, 4).into(), 134 | Line::new((0, 3), (3, 0)).into()]; 135 | 136 | let mut r = BinaryBuilder::new().width(4).height(5).build()?; 137 | for shape in shapes { 138 | r.rasterize(&shape)?; 139 | } 140 | 141 | let pixels = r.finish(); 142 | assert_eq!( 143 | pixels.mapv(|v| v as u8), 144 | array![ 145 | [0, 0, 1, 0], 146 | [0, 1, 1, 0], 147 | [1, 1, 0, 0], 148 | [1, 0, 0, 0], 149 | [0, 0, 0, 1] 150 | ] 151 | ); 152 | # Ok(())} 153 | ``` 154 | 155 | ## Labeling (non-binary rasterization) 156 | 157 | So far we've been generating binary arrays; what if you want to 158 | rasterize different shapes to the same integer array, storing a 159 | different value corresponding to each shape for each pixel? For that, 160 | we have [Rasterizer] which we construct using 161 | [LabelBuilder]. When you burn a shape with [Rasterizer] you 162 | provide not just the shape, but also a foreground label. But before 163 | you can burn anything, you have to specify a background label used to 164 | fill the empty raster array. 165 | 166 | ```rust 167 | # use geo_rasterize::{Result, LabelBuilder, Rasterizer}; 168 | # fn main() -> Result<()> { 169 | use geo::{Geometry, Line, Point}; 170 | use ndarray::array; 171 | 172 | let point = Point::new(3, 4); 173 | let line = Line::new((0, 3), (3, 0)); 174 | 175 | let mut rasterizer = LabelBuilder::background(0).width(4).height(5).build()?; 176 | rasterizer.rasterize(&point, 3)?; 177 | rasterizer.rasterize(&line, 7)?; 178 | 179 | let pixels = rasterizer.finish(); 180 | assert_eq!( 181 | pixels.mapv(|v| v as u8), 182 | array![ 183 | [0, 0, 7, 0], 184 | [0, 7, 7, 0], 185 | [7, 7, 0, 0], 186 | [7, 0, 0, 0], 187 | [0, 0, 0, 3] 188 | ] 189 | ); 190 | # Ok(())} 191 | ``` 192 | 193 | ## Heatmaps 194 | 195 | What happens if two shapes touch the same pixel? In the example above, 196 | the last shape written wins. But you can change that behavior by 197 | specifying a different value for [MergeAlgorithm] using 198 | [LabelBuilder::algorithm]. In fact, using [MergeAlgorithm::Add], you 199 | can easily make a heat map showing the shape density where each pixel value tells 200 | you the number of shapes that landed on it! 201 | 202 | ```rust 203 | # use geo_rasterize::{Result, LabelBuilder, Rasterizer, MergeAlgorithm}; 204 | # use geo::{Geometry, Line, Point}; 205 | # use ndarray::array; 206 | # fn main() -> Result<()> { 207 | 208 | let lines = vec![Line::new((0, 0), (5, 5)), Line::new((5, 0), (0, 5))]; 209 | 210 | let mut rasterizer = LabelBuilder::background(0) 211 | .width(5) 212 | .height(5) 213 | .algorithm(MergeAlgorithm::Add) 214 | .build()?; 215 | for line in lines { 216 | rasterizer.rasterize(&line, 1)?; 217 | } 218 | 219 | let pixels = rasterizer.finish(); 220 | assert_eq!( 221 | pixels.mapv(|v| v as u8), 222 | array![ 223 | [1, 0, 0, 0, 1], 224 | [0, 1, 0, 1, 1], 225 | [0, 0, 2, 1, 0], 226 | [0, 1, 1, 1, 0], 227 | [1, 1, 0, 0, 1] 228 | ] 229 | ); 230 | 231 | # Ok(())} 232 | ``` 233 | 234 | Two lines cross at the center where you'll find `2`. Note that 235 | [Rasterizer] is not limited to integers; any copyable type that 236 | can be added will do. [Rasterizer] offers similar functionality 237 | to [rasterio](https://rasterio.readthedocs.io/)'s 238 | [features.rasterize](https://rasterio.readthedocs.io/en/latest/api/rasterio.features.html#rasterio.features.rasterize) 239 | function. 240 | 241 | 242 | ## Geographic transforms 243 | 244 | All our examples so far have assumed that our shapes' coordinates are 245 | in the image space. In other words, we've assumed that the `x` 246 | coordinates will be in the range `0..width` and the `y` coordinates 247 | will be in the range `0..height`. Alas, that is often not the case! 248 | 249 | For satellite imagery (or remote sensing imagery in general), images 250 | will almost always specify both a Coordinate Reference System 251 | ([CRS](https://en.wikipedia.org/wiki/Spatial_reference_system)) and an 252 | affine transformation in their metadata. See [rasterio's 253 | Georeferencing](https://rasterio.readthedocs.io/en/latest/topics/georeferencing.html) 254 | for more details. 255 | 256 | In order to work with most imagery, you have to convert your vector 257 | shapes from whatever their original CRS is (often `EPSG:4326` for 258 | geographic longitude and latitude) into whatever CRS your data file 259 | specifies (often a 260 | [UTM](https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system) 261 | projection but there are so many choices). Then, you need to apply an 262 | affine transformation to convert from world coordinates to pixel 263 | coordinates. Since raster imagery usually specifies the inverse 264 | transformation matrix (i.e. a `pix_to_geo` transform), you'll first 265 | need to invert it to get a `geo_to_pix` transform before applying it 266 | to the coordinates. And now you've got pixel coordinates appropriate 267 | for your image data! 268 | 269 | [BinaryRasterizer] and [Rasterizer] can ease this tedious process 270 | by taking care of the affine transformation. Make sure to pass a 271 | [Transform] object to [BinaryBuilder] or [LabelBuilder]. In either 272 | case, that transform is a `geo_to_pix` transform, which means you'll 273 | have to: 274 | 275 | * extract the CRS from your image and convert your shapes into that 276 | CRS (probably using [the proj 277 | crate](https://docs.rs/proj/0.24.0/proj/index.html#integration-with-geo-types) 278 | and its integration with [geo types][geo], 279 | * extract the `pix_to_geo` transform from your imagery metadata 280 | * create a [Transform] instance from that data (GDAL represents these 281 | as a `[f64; 6]` array) 282 | * call `transform.inverse` to get the corresponding `geo_to_pix` 283 | transform (since not all transforms are invertible, `inverse` gives 284 | you an `Option`) 285 | * pass the resulting [Transform] to either [BinaryBuilder] or 286 | [LabelBuilder]. 287 | 288 | ## Performance 289 | 290 | For polygons, our runtime is `O(S * P * log(P))` where `S` is the 291 | number of scanlines (the polygon's vertical extent in pixels) and `P` 292 | is the number of coordinates in the polygon exterior and all its 293 | holes. Memory consumption is approximately `P` machine words. Because 294 | runtime depends so heavily on the number of coordinates, simplifying 295 | polygons before rasterization can speed up rasterization dramatically, 296 | especially in cases where polygons have very high resolution compared 297 | to the pixel size. 298 | 299 | For other shapes, runtime is proportional to the number of pixels 300 | filled in. 301 | 302 | 303 | 304 | 305 | ## Why not GDAL? 306 | 307 | GDAL is the swiss army chainsaw of geospatial data processing. It 308 | handles vector data (by wrapping `libgeos`) and many data formats. The 309 | version that ships with Ubuntu 21.10 links to 115 shared libraries 310 | which includes support for handling PDF files, Excel spreadsheets, 311 | curl, Kerberos, ODBC, several XML libraries, a linear algebra solver, 312 | several cryptographic packages, and on and on and on. GDAL is a giant 313 | pile of C and C++ code slapped together in a fragile 314 | assembly. Building GDAL is a rather unpleasant since even a stripped 315 | down version depends on a bunch of other C and C++ packages. If you 316 | want to quickly build and deploy a static binary for AWS Lambda, rust 317 | makes that really easy, right up until you need GDAL. Then things get 318 | really really difficult. 319 | 320 | Speaking of rust, I've been bitten multiple times in my career now 321 | with GDAL data race bugs that rust just forbids. I'm so tired. 322 | 323 | Configuring GDAL is deeply unpleasant. Quick! Look at the [GDAL 324 | configuration guide](https://gdal.org/user/configoptions.html) and 325 | tell me which of the 170ish configuration knobs I need to adjust to 326 | control GDAL's caching so that a lambda function that uses GDAL won't 327 | leak memory due to image caching? Ha! That's a trick question because 328 | you need multiple tunables to control the different caches. That's 329 | what you expect for a 23 year old 2.5 MLOC software library. 330 | 331 | For a more pythonic perspective on the noGDAL movement, check out 332 | [Kipling Crossing](https://kipcrossing.github.io/2021-01-03-noGDAL/). 333 | 334 | ## Alternative crates 335 | 336 | * [GDAL](https://docs.rs/gdal/latest/gdal/raster/fn.rasterize.html) 337 | can rasterize but then you'll need to bring in GDAL which is 338 | difficult to deal with. 339 | * [raqote](https://crates.io/crates/raqote) is a powerful 2D 340 | rasterizer intended for graphics. 341 | * [rasterize](https://crates.io/crates/rasterize) is another pure rust 342 | library, but less mature than `raqote`. 343 | 344 | 345 | ## Contributing 346 | 347 | Contributions are welcome! Have a look at the 348 | [issues](https://github.com/msalib/geo-rasterize/issues), and open a 349 | pull request if you'd like to add an algorithm or some functionality. 350 | 351 | ## License 352 | 353 | Licensed under either of 354 | 355 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 356 | http://www.apache.org/licenses/LICENSE-2.0) 357 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 358 | http://opensource.org/licenses/MIT) 359 | 360 | at your option. 361 | 362 | ### Contribution 363 | 364 | Unless you explicitly state otherwise, any contribution intentionally 365 | submitted for inclusion in the work by you, as defined in the 366 | Apache-2.0 license, shall be dual licensed as above, without any 367 | additional terms or conditions. 368 | -------------------------------------------------------------------------------- /benches/us-county-2018.fgb: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:caad1723d43336087bc5106591f472022204a7d0254470d61b91ccc73b079147 3 | size 13738416 4 | -------------------------------------------------------------------------------- /benches/versus-gdal.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::BufReader}; 2 | 3 | use anyhow::Result; 4 | use criterion::{criterion_group, criterion_main, Criterion}; 5 | use flatgeobuf::{FallibleStreamingIterator, FgbReader}; 6 | use geo::map_coords::MapCoordsInplace; 7 | use geo::prelude::*; 8 | use geo::Geometry; 9 | use geo::Polygon; 10 | use geo_rasterize::LabelBuilder; 11 | use geo_rasterize::{MergeAlgorithm, Rasterize, Rasterizer}; 12 | use geozero::ToGeo; 13 | use ndarray::Array2; 14 | use rand::prelude::*; 15 | 16 | fn load() -> Result>> { 17 | let mut f = BufReader::new(File::open("benches/us-county-2018.fgb")?); 18 | let mut fgb = FgbReader::open(&mut f)?; 19 | let mut result = Vec::with_capacity(fgb.select_all()?); 20 | while let Some(feature) = fgb.next()? { 21 | match feature.to_geo()? { 22 | Geometry::Polygon(poly) => { 23 | result.push(poly); 24 | } 25 | Geometry::MultiPolygon(mp) => mp.iter().for_each(|poly| { 26 | result.push(poly.clone()); 27 | }), 28 | _ => {} 29 | } 30 | } 31 | Ok(result) 32 | } 33 | 34 | const WIDTH: usize = 501; 35 | const HEIGHT: usize = 500; 36 | const LEN: usize = 1_00; 37 | 38 | fn transform(shapes: &[Polygon]) -> Result>> { 39 | let mut rng = rand::thread_rng(); 40 | let mut result = Vec::with_capacity(shapes.len()); 41 | for shape in shapes { 42 | let angle = rng.gen_range(0. ..360.); 43 | let mut shape = shape.rotate(angle); 44 | 45 | // recenter at origin 46 | let center = shape.centroid().unwrap(); 47 | shape.translate_inplace(-center.x(), -center.y()); 48 | 49 | // scale 50 | let bounds = shape.bounding_rect().unwrap(); 51 | let scale_factor = rng.gen_range(0.01..10.) * (WIDTH.max(HEIGHT) as f64) 52 | / bounds.width().max(bounds.height()); 53 | shape.map_coords_inplace(|&(x, y)| (scale_factor * x, scale_factor * y)); 54 | shape.translate_inplace( 55 | rng.gen_range(0..WIDTH) as f64, 56 | rng.gen_range(0..HEIGHT) as f64, 57 | ); 58 | if result.len() > LEN { 59 | break; 60 | } 61 | result.push(shape); 62 | } 63 | Ok(result) 64 | } 65 | 66 | fn i_rasterize(shapes: &[Polygon]) -> Result> { 67 | let mut r = LabelBuilder::background(0.) 68 | .width(WIDTH) 69 | .height(HEIGHT) 70 | .algorithm(MergeAlgorithm::Add) 71 | .build()?; 72 | shapes.iter().try_for_each(|shape| r.rasterize(shape, 1.))?; 73 | Ok(r.finish()) 74 | } 75 | 76 | #[path = "../src/tests/utils.rs"] 77 | mod utils; 78 | use utils::gdal_rasterize; 79 | 80 | fn criterion_benchmark(c: &mut Criterion) { 81 | let mut group = c.benchmark_group("yo"); 82 | group.sample_size(10); 83 | 84 | let shapes = transform(&load().unwrap()).unwrap(); 85 | 86 | group.bench_function("me", |b| b.iter(|| i_rasterize(&shapes))); 87 | group.bench_function("gdal", |b| { 88 | b.iter(|| gdal_rasterize(WIDTH, HEIGHT, &shapes, MergeAlgorithm::Add)) 89 | }); 90 | } 91 | 92 | criterion_group!(benches, criterion_benchmark); 93 | criterion_main!(benches); 94 | 95 | // used for profiling with perf+hotspt: 96 | 97 | // pub fn main() { 98 | // let shapes = transform(&load().unwrap()).unwrap(); 99 | // dbg!(i_rasterize(&shapes).unwrap().sum()); 100 | //} 101 | -------------------------------------------------------------------------------- /proptest-regressions/proptests.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 3b6b8c4f6e2bd8db2789acfc899df80caad00840fdca48052194109633e7b26b # shrinks to shape = [LineString(LineString([Coordinate { x: 0.0, y: 0.995529841217325 }, Coordinate { x: 14.345339055640835, y: 1.003085512751344 }]))] 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | use std::{collections::HashSet, fmt::Debug, ops::Add}; 3 | 4 | use euclid::{Transform2D, UnknownUnit}; 5 | use geo::{ 6 | algorithm::{ 7 | coords_iter::CoordsIter, 8 | map_coords::{MapCoords, MapCoordsInplace}, 9 | }, 10 | Geometry, GeometryCollection, Line, LineString, MultiLineString, MultiPoint, MultiPolygon, 11 | Point, Polygon, Rect, Triangle, 12 | }; 13 | use ndarray::s; 14 | use ndarray::Array2; 15 | use num_traits::{Num, NumCast}; 16 | use thiserror::Error; 17 | 18 | mod poly; 19 | use poly::rasterize_polygon; 20 | mod line; 21 | use line::rasterize_line; 22 | #[cfg(test)] 23 | mod proptests; 24 | 25 | /// Affine transform that describes how to convert world-space 26 | /// coordinates to pixel coordinates. 27 | pub type Transform = Transform2D; 28 | type EuclidPoint = euclid::Point2D; 29 | 30 | /// Error type for this crate 31 | #[derive(Error, Clone, Debug, PartialEq, Eq)] 32 | pub enum RasterizeError { 33 | /// at least one coordinate of the supplied geometry is NaN or infinite 34 | #[error("at least one coordinate of the supplied geometry is NaN or infinite")] 35 | NonFiniteCoordinate, 36 | 37 | /// `width` is required in builder 38 | #[error("`width` is required in builder")] 39 | MissingWidth, 40 | 41 | /// `height` is required in builder 42 | #[error("`height` is required in builder")] 43 | MissingHeight, 44 | } 45 | 46 | /// Result type for this crate that uses [RasterizeError]. 47 | pub type Result = std::result::Result; 48 | 49 | /// A builder that can construct instances of [BinaryRasterizer], a 50 | /// rasterizer that can rasterize shapes into a 2-dimensional array of 51 | /// booleans. 52 | /// 53 | /// ```rust 54 | /// # use geo_rasterize::{Result, BinaryBuilder, BinaryRasterizer}; 55 | /// # fn main() -> Result<()> { 56 | /// let rasterizer: BinaryRasterizer = BinaryBuilder::new().width(37).height(21).build()?; 57 | /// # Ok(())} 58 | /// ``` 59 | #[derive(Debug, Clone, Default)] 60 | pub struct BinaryBuilder { 61 | width: Option, 62 | height: Option, 63 | geo_to_pix: Option, 64 | } 65 | 66 | impl BinaryBuilder { 67 | pub fn new() -> Self { 68 | BinaryBuilder::default() 69 | } 70 | 71 | pub fn width(mut self, width: usize) -> Self { 72 | self.width = Some(width); 73 | self 74 | } 75 | 76 | pub fn height(mut self, height: usize) -> Self { 77 | self.height = Some(height); 78 | self 79 | } 80 | 81 | pub fn geo_to_pix(mut self, geo_to_pix: Transform) -> Self { 82 | self.geo_to_pix = Some(geo_to_pix); 83 | self 84 | } 85 | 86 | pub fn build(self) -> Result { 87 | match (self.width, self.height) { 88 | (None, _) => Err(RasterizeError::MissingWidth), 89 | (_, None) => Err(RasterizeError::MissingHeight), 90 | (Some(width), Some(height)) => BinaryRasterizer::new(width, height, self.geo_to_pix), 91 | } 92 | } 93 | } 94 | 95 | /// A rasterizer that burns shapes into a 2-dimensional boolean 96 | /// array. It can be built either by calling 97 | /// [new][BinaryRasterizer::new] or using [BinaryBuilder]. 98 | /// 99 | /// Each Rasterizer requires a `width` and `height` measured in pixels 100 | /// that describe the shape of the output array. They can optionally 101 | /// take an affine transform that describes how to convert world-space 102 | /// coordinates into pixel-space coordinates. When a transformer is 103 | /// supplied, all of its parameters must be finite or 104 | /// [RasterizeError::NonFiniteCoordinate] will be returned. 105 | /// 106 | /// ```rust 107 | /// # use geo_rasterize::{Result, BinaryBuilder, BinaryRasterizer}; 108 | /// # fn main() -> Result<()> { 109 | /// use geo::{Geometry, Line, Point}; 110 | /// use ndarray::array; 111 | /// use geo_rasterize::BinaryBuilder; 112 | /// 113 | /// let shapes: Vec> = 114 | /// vec![Point::new(3, 4).into(), 115 | /// Line::new((0, 3), (3, 0)).into()]; 116 | /// 117 | /// let mut r = BinaryBuilder::new().width(4).height(5).build()?; 118 | /// for shape in shapes { 119 | /// r.rasterize(&shape)?; 120 | /// } 121 | /// 122 | /// let pixels = r.finish(); 123 | /// assert_eq!( 124 | /// pixels.mapv(|v| v as u8), 125 | /// array![ 126 | /// [0, 0, 1, 0], 127 | /// [0, 1, 1, 0], 128 | /// [1, 1, 0, 0], 129 | /// [1, 0, 0, 0], 130 | /// [0, 0, 0, 1] 131 | /// ] 132 | /// ); 133 | /// # Ok(())} 134 | /// ``` 135 | #[derive(Clone, Debug)] 136 | pub struct BinaryRasterizer { 137 | inner: Rasterizer, 138 | } 139 | 140 | fn to_float(coords: &(T, T)) -> (f64, f64) 141 | where 142 | T: Into + Copy, 143 | { 144 | (coords.0.into(), coords.1.into()) 145 | } 146 | 147 | impl BinaryRasterizer { 148 | pub fn new(width: usize, height: usize, geo_to_pix: Option) -> Result { 149 | let non_finite = geo_to_pix 150 | .map(|geo_to_pix| geo_to_pix.to_array().iter().any(|param| !param.is_finite())) 151 | .unwrap_or(false); 152 | if non_finite { 153 | Err(RasterizeError::NonFiniteCoordinate) 154 | } else { 155 | let inner = Rasterizer::new(width, height, geo_to_pix, MergeAlgorithm::Replace, 0); 156 | Ok(BinaryRasterizer { inner }) 157 | } 158 | } 159 | 160 | /// Retrieve the transform. 161 | pub fn geo_to_pix(&self) -> Option { 162 | self.inner.geo_to_pix 163 | } 164 | 165 | /// Rasterize one shape, which can be any type that [geo] provides 166 | /// using any coordinate numeric type that can be converted into 167 | /// `f64`. 168 | pub fn rasterize(&mut self, shape: &InputShape) -> Result<()> 169 | where 170 | InputShape: MapCoords, 171 | ShapeAsF64: Rasterize + for<'a> CoordsIter<'a, Scalar = f64> + MapCoordsInplace, 172 | Coord: Into + Copy + Debug + Num + NumCast + PartialOrd, 173 | { 174 | // first, convert our input shape so that its coordinates are of type f64 175 | self.inner.rasterize(shape, 1) 176 | } 177 | 178 | /// Retrieve the completed raster array. 179 | pub fn finish(self) -> Array2 { 180 | self.inner 181 | .finish() 182 | .mapv(|v| if v == 1u8 { true } else { false }) 183 | } 184 | } 185 | 186 | #[doc(hidden)] 187 | pub trait Rasterize