diff --git a/poetry.lock b/poetry.lock index 4e40015e..c9102c02 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3430,6 +3430,130 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "shapely" +version = "2.0.2" +description = "Manipulation and analysis of geometric objects" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shapely-2.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6ca8cffbe84ddde8f52b297b53f8e0687bd31141abb2c373fd8a9f032df415d6"}, + {file = "shapely-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:baa14fc27771e180c06b499a0a7ba697c7988c7b2b6cba9a929a19a4d2762de3"}, + {file = "shapely-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36480e32c434d168cdf2f5e9862c84aaf4d714a43a8465ae3ce8ff327f0affb7"}, + {file = "shapely-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef753200cbffd4f652efb2c528c5474e5a14341a473994d90ad0606522a46a2"}, + {file = "shapely-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a41ff4323fc9d6257759c26eb1cf3a61ebc7e611e024e6091f42977303fd3a"}, + {file = "shapely-2.0.2-cp310-cp310-win32.whl", hash = "sha256:72b5997272ae8c25f0fd5b3b967b3237e87fab7978b8d6cd5fa748770f0c5d68"}, + {file = "shapely-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:34eac2337cbd67650248761b140d2535855d21b969d76d76123317882d3a0c1a"}, + {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b0c052709c8a257c93b0d4943b0b7a3035f87e2d6a8ac9407b6a992d206422f"}, + {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d217e56ae067e87b4e1731d0dc62eebe887ced729ba5c2d4590e9e3e9fdbd88"}, + {file = "shapely-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94ac128ae2ab4edd0bffcd4e566411ea7bdc738aeaf92c32a8a836abad725f9f"}, + {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3ee28f5e63a130ec5af4dc3c4cb9c21c5788bb13c15e89190d163b14f9fb89"}, + {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:737dba15011e5a9b54a8302f1748b62daa207c9bc06f820cd0ad32a041f1c6f2"}, + {file = "shapely-2.0.2-cp311-cp311-win32.whl", hash = "sha256:45ac6906cff0765455a7b49c1670af6e230c419507c13e2f75db638c8fc6f3bd"}, + {file = "shapely-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:dc9342fc82e374130db86a955c3c4525bfbf315a248af8277a913f30911bed9e"}, + {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:06f193091a7c6112fc08dfd195a1e3846a64306f890b151fa8c63b3e3624202c"}, + {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eebe544df5c018134f3c23b6515877f7e4cd72851f88a8d0c18464f414d141a2"}, + {file = "shapely-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e92e7c255f89f5cdf777690313311f422aa8ada9a3205b187113274e0135cd8"}, + {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be46d5509b9251dd9087768eaf35a71360de6afac82ce87c636990a0871aa18b"}, + {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5533a925d8e211d07636ffc2fdd9a7f9f13d54686d00577eeb11d16f00be9c4"}, + {file = "shapely-2.0.2-cp312-cp312-win32.whl", hash = "sha256:084b023dae8ad3d5b98acee9d3bf098fdf688eb0bb9b1401e8b075f6a627b611"}, + {file = "shapely-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ea84d1cdbcf31e619d672b53c4532f06253894185ee7acb8ceb78f5f33cbe033"}, + {file = "shapely-2.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed1e99702125e7baccf401830a3b94d810d5c70b329b765fe93451fe14cf565b"}, + {file = "shapely-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d897e6bdc6bc64f7f65155dbbb30e49acaabbd0d9266b9b4041f87d6e52b3a"}, + {file = "shapely-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0521d76d1e8af01e712db71da9096b484f081e539d4f4a8c97342e7971d5e1b4"}, + {file = "shapely-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:5324be299d4c533ecfcfd43424dfd12f9428fd6f12cda38a4316da001d6ef0ea"}, + {file = "shapely-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:78128357a0cee573257a0c2c388d4b7bf13cb7dbe5b3fe5d26d45ebbe2a39e25"}, + {file = "shapely-2.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87dc2be34ac3a3a4a319b963c507ac06682978a5e6c93d71917618b14f13066e"}, + {file = "shapely-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:42997ac806e4583dad51c80a32d38570fd9a3d4778f5e2c98f9090aa7db0fe91"}, + {file = "shapely-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ccfd5fa10a37e67dbafc601c1ddbcbbfef70d34c3f6b0efc866ddbdb55893a6c"}, + {file = "shapely-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7c95d3379ae3abb74058938a9fcbc478c6b2e28d20dace38f8b5c587dde90aa"}, + {file = "shapely-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a21353d28209fb0d8cc083e08ca53c52666e0d8a1f9bbe23b6063967d89ed24"}, + {file = "shapely-2.0.2-cp38-cp38-win32.whl", hash = "sha256:03e63a99dfe6bd3beb8d5f41ec2086585bb969991d603f9aeac335ad396a06d4"}, + {file = "shapely-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:c6fd29fbd9cd76350bd5cc14c49de394a31770aed02d74203e23b928f3d2f1aa"}, + {file = "shapely-2.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f217d28ecb48e593beae20a0082a95bd9898d82d14b8fcb497edf6bff9a44d7"}, + {file = "shapely-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:394e5085b49334fd5b94fa89c086edfb39c3ecab7f669e8b2a4298b9d523b3a5"}, + {file = "shapely-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd3ad17b64466a033848c26cb5b509625c87d07dcf39a1541461cacdb8f7e91c"}, + {file = "shapely-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d41a116fcad58048d7143ddb01285e1a8780df6dc1f56c3b1e1b7f12ed296651"}, + {file = "shapely-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dea9a0651333cf96ef5bb2035044e3ad6a54f87d90e50fe4c2636debf1b77abc"}, + {file = "shapely-2.0.2-cp39-cp39-win32.whl", hash = "sha256:b8eb0a92f7b8c74f9d8fdd1b40d395113f59bd8132ca1348ebcc1f5aece94b96"}, + {file = "shapely-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:794affd80ca0f2c536fc948a3afa90bd8fb61ebe37fe873483ae818e7f21def4"}, + {file = "shapely-2.0.2.tar.gz", hash = "sha256:1713cc04c171baffc5b259ba8531c58acc2a301707b7f021d88a15ed090649e7"}, +] + +[package.dependencies] +numpy = ">=1.14" + +[package.extras] +docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "shapelysmooth" +version = "0.1.1" +description = "A polyline smoothing package for shapely." +optional = false +python-versions = ">=3.6" +files = [ + {file = "shapelysmooth-0.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92c46441ce24a5b3faa85abf1b28de3cbf5a41a6e6cfd1f52aa984643689cc59"}, + {file = "shapelysmooth-0.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41ec6c2fc104c2a93d0c775a8863d1b1c9a958f01981c6d3e76e0e74baafd415"}, + {file = "shapelysmooth-0.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39af9c8c517f00b0cfad43489835b5bbc1fd55ef671df2d6349c57b54ec410d"}, + {file = "shapelysmooth-0.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e350c979a331ccdb47082d2ef1a8e86dcf951ae7a4ecdb4e31bddef33b1e5c3"}, + {file = "shapelysmooth-0.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d8dab27c086fd2bbc48c560c77649c77ba901b4a1718e6d568792bcef4f958e2"}, + {file = "shapelysmooth-0.1.1-cp310-cp310-win32.whl", hash = "sha256:5161b2e836f9186c25cef6b63cb5fd3c927902636746f21837884d67bb5166a5"}, + {file = "shapelysmooth-0.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:f086804428ca430e0b21557dc0d0df51e29c712dec7f652e9fecffa153b621c1"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5958d6371ac41f06c701384ce560067800c66f68e6b32d4b3332a18f0f030e5"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c53529dc97079ae684e5bb3b690ee8a2da3e837bbf57d9e1e441a2c2d6646597"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a783d6bdcd2511da767f19ce3c02d4af0b65d35347980b1831fa1a576b3ded4"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ed97f0f6b3549dc442ac1efea7f347344e476afa5a5f2327242822ca03d6be7a"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:815a087ed3bc5d5c4fae9b3c26dfb2db81593435d786ee1b88376958b5e4a3d7"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-win32.whl", hash = "sha256:75614eda204cdcb3432b5c235e2c0203d54ffb383c6f8bc2b762de2856489498"}, + {file = "shapelysmooth-0.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:3b96b2e6d3503b177fa2c3609cccc393aff8dce67777d2e5388dfc15039a4eb0"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:844b676690906f917c9d92354375361efbeef1e81c656bfc7ec43a7085d10756"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:273400dff7527f681cb68dc2b4f056b167ba3db20ec6fc05f74ad6cabc4d653a"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7bc1fdbdb36255492da3ff0df62a2b270132e3599ed7ec2fa8f52d7a2d715a7"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:35c7c6037e0479368606f41200c206d4f4e8594fd722887cd30348716def0abd"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8008c205614264bb402e598fc9ca54e1948d42a329d9d42596503242c97681af"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-win32.whl", hash = "sha256:cdd272512a2cd364762e3b97e929cd6c8f31fbfe6048bff84a3e707ebba211d8"}, + {file = "shapelysmooth-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dd20cd8c19b8e0889b14f9ea22ec3584c002e4d42a931db9537cde13bf400006"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:17719a43a0e82f7a9621bf39a94db2e14979d6653a5a4761edc3bb4c46ced88a"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3aa7a593deb5301989d76381a2acc06dcf3fa790dd0ff78e390605801dc1bf40"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0abea38c8042d6df4a057ebb371875ed50d1a4bdbf17c5924a06d1ed760a80d4"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1919d13050265cbb4e8c952d4a206cb7d42acd610c68ddc247f69ac87517154c"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:65a8cd90867961767e45f84247ecf7f1805cd25f98c4eafb96349baafb03bffc"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-win32.whl", hash = "sha256:8aa29f0eb27a846d991e16528d459fcfe0a97505ac6372c301b485e7fee9fe7c"}, + {file = "shapelysmooth-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54d3acd8537a1324c907562a71ab70f21a4ef7afe2be8ae40ec2710f16f85ff6"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:273cb4706467d48ef1e3c1dd3a56149e0f7a8a8bd33529a696a8bac45c875ee8"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0762e2fd1ff4757c1795c2dadf044d3748ea222fbbaededddf3025501609f805"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8179469ca05a20d979dc92a86006692fce843959d66b3636f6f0bdccef00ff1b"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d043bd76069cd4660e958190361899dd6064a348ed87ff904f2f2259d1cebcf0"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4a2acb770d088e810e139f5016d619e22cbe68b4d9aad821a07c651df4f460f0"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-win32.whl", hash = "sha256:1172c956d5fa95b4c9c6f9c5c50f6b595513a3c29823d52364c5491d05b8193c"}, + {file = "shapelysmooth-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:bc4b31c9dc90cf2cf8f333bb2112553680cbd420bc139552e32741013eb6bdb9"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:27028b6c399b215bc4dda1e516f83b7a88e076ae5e2c1cebb457e8c9d89cf709"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d8aac3acc2cdd7537d6b3ba17cd7b5fdfe6bed42168e1320f26fed8cf874e47"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9893f727194d1aa18247f8d5d992651a815aa7c25dd79398f8f80fdb29762b"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4aeb697c168ef12d2f08762e95c54662b04a611a3498e743ff16fb4bf977533e"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:844aa8bc73b480ec38aa74e0f3a0993f6d80c43f1dfe49cde0bba4bf00e138b3"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-win32.whl", hash = "sha256:5a1dc78312cc0a96892bf053a310b4989874b77d46dc0b084fc0b07cae5f5ef7"}, + {file = "shapelysmooth-0.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:99b9edc90e4ce795da4c87fa80a5f4a584f8aa08384e2eb82d040ad6d228c180"}, + {file = "shapelysmooth-0.1.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:78a9c16105353383821b49b99785e5c1f0fa41d996e47a05cb43be76c24e189d"}, + {file = "shapelysmooth-0.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:847f9422a51c7cb38b95938687ac8b82b55228904009f76416743d5b6b11b96d"}, + {file = "shapelysmooth-0.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3efe8dd9e570b8f86c534f9ea989e4e7ae4575ff6999bf81cea54baea7d94341"}, + {file = "shapelysmooth-0.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4c9f65e5d2b892af5150b6ba941414d205d75d2af0bc9a07c90dcb8571b82e27"}, + {file = "shapelysmooth-0.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ef9ae7ff66de6e3ea9056586a1f7cc4bdb9ab74bac47187c6d4f9565221faeec"}, + {file = "shapelysmooth-0.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26304a6861b47b2062fe95325e7424c727f0051fbd8525ad5ddd30966496384b"}, + {file = "shapelysmooth-0.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d277ae4adc694be0403b95163564acc481538b9172f012ca2d7a13769eb3c23"}, + {file = "shapelysmooth-0.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:86b4f26da0366080ea1bcd0325075d144c395ca31ac0a60e2ec38b873e43c456"}, + {file = "shapelysmooth-0.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:26698a2023d5c11f70069c9a891b68b478a48fe3b4cb5454f270ae929e01de4f"}, + {file = "shapelysmooth-0.1.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56d306d6f8d3219e0db7bbcc8cf58c7f6e9a1e66786603fb1f050f8eec080aed"}, + {file = "shapelysmooth-0.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e08cfaa8ea59b7b9134984af698a83c9b10fd569633866b8eb3963ebd8cf601"}, + {file = "shapelysmooth-0.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:45d765b3f0cc7832b0d6ab2052afbb8f6d783d1dd5cbe0fec4df997f143af61b"}, + {file = "shapelysmooth-0.1.1.tar.gz", hash = "sha256:02141a11f8f615b418a854475292c107b3e2c6bc5f56e9b683eea9f63ac288a3"}, +] + +[package.dependencies] +shapely = "*" + [[package]] name = "simplejson" version = "3.19.2" @@ -4445,4 +4569,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "~3.10" -content-hash = "8ad3646831b3e646e4f451e2ece91ecebf75c0ed8c19231a606bd46692ac60d3" +content-hash = "a16b51e7579ac3fd5022c381f41b77d052039d2c87d02e8535d81575ee18e832" diff --git a/pyproject.toml b/pyproject.toml index e14ae77e..e3f5d723 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ torchvision = {version = "^0.16.0+cpu", source = "pytorch"} ultralytics = "^8.0.209" segment-anything = {git = "https://github.com/facebookresearch/segment-anything.git"} pyzbar = "^0.1.9" +shapelysmooth = "^0.1.1" [tool.poetry.group.dev.dependencies] # Versions are fixed to match versions used by pre-commit diff --git a/sketch_map_tool/tasks.py b/sketch_map_tool/tasks.py index dbf9b697..b7ca6a4c 100644 --- a/sketch_map_tool/tasks.py +++ b/sketch_map_tool/tasks.py @@ -2,7 +2,6 @@ from uuid import UUID from zipfile import ZipFile -import geojson from celery.result import AsyncResult from celery.signals import worker_process_init, worker_process_shutdown from geojson import FeatureCollection @@ -18,12 +17,11 @@ from sketch_map_tool.oqt_analyses import generate_pdf as generate_report_pdf from sketch_map_tool.oqt_analyses import get_report from sketch_map_tool.upload_processing import ( - clean, clip, - enrich, georeference, merge, polygonize, + post_process, ) from sketch_map_tool.upload_processing.detect_markings import detect_markings from sketch_map_tool.upload_processing.ml_models import init_model @@ -92,13 +90,7 @@ def georeference_sketch_maps( bboxes: list[Bbox], ) -> AsyncResult | BytesIO: def process(sketch_map_id: int, uuid: str, bbox: Bbox) -> BytesIO: - """Process a Sketch Map. - - :param sketch_map_id: ID under which uploaded file is stored in the database. - :param uuid: UUID under which the sketch map was created. - :bbox: Bounding box of the AOI on the sketch map. - :return: Georeferenced image (GeoTIFF) of the sketch map . - """ + """Process a Sketch Map.""" # r = interim result r = db_client_celery.select_file(sketch_map_id) r = to_array(r) @@ -129,36 +121,23 @@ def digitize_sketches( # Initialize ml-models. This has to happen inside of celery context. # Custom trained model for object detection of markings and colors yolo_path = init_model(get_config_value("neptune_model_id_yolo")) - yolo_model = YOLO(yolo_path) + yolo_model: YOLO = YOLO(yolo_path) # Zero shot segment anything model sam_path = init_model(get_config_value("neptune_model_id_sam")) sam_model = sam_model_registry["vit_b"](sam_path) - sam_predictor = SamPredictor(sam_model) # mask predictor - - def process( - sketch_map_id: int, - name: str, - uuid: str, - bbox: Bbox, - sam_predictor: SamPredictor, - yolo_model: YOLO, - ) -> FeatureCollection: - """Process a Sketch Map.""" + sam_predictor: SamPredictor = SamPredictor(sam_model) # mask predictor + + l = [] # noqa: E741 + for file_id, file_name, uuid, bbox in zip(file_ids, file_names, uuids, bboxes): # r = interim result - r: BytesIO = db_client_celery.select_file(sketch_map_id) # type: ignore + r: BytesIO = db_client_celery.select_file(file_id) # type: ignore r: NDArray = to_array(r) # type: ignore r: NDArray = clip(r, map_frames[uuid]) # type: ignore r: NDArray = detect_markings(r, yolo_model, sam_predictor) # type: ignore - r: BytesIO = georeference(r, bbox, bgr=False) # type: ignore - r: BytesIO = polygonize(r, name) # type: ignore - r: FeatureCollection = geojson.load(r) # type: ignore - r: FeatureCollection = clean(r) # type: ignore - r: FeatureCollection = enrich(r, {"name": name}) # type: ignore - return r - - return merge( - [ - process(file_id, name, uuid, bbox, sam_predictor, yolo_model) - for file_id, name, uuid, bbox in zip(file_ids, file_names, uuids, bboxes) - ] - ) + # m = marking + for m in r: + m: BytesIO = georeference(m, bbox, bgr=False) # type: ignore + m: FeatureCollection = polygonize(m, layer_name=file_name) # type: ignore + m: FeatureCollection = post_process(m, file_name) + l.append(m) + return merge(l) diff --git a/sketch_map_tool/upload_processing/__init__.py b/sketch_map_tool/upload_processing/__init__.py index c8f77909..14aa3e45 100644 --- a/sketch_map_tool/upload_processing/__init__.py +++ b/sketch_map_tool/upload_processing/__init__.py @@ -1,19 +1,17 @@ -from .clean import clean from .clip import clip from .detect_markings import detect_markings -from .enrich import enrich from .georeference import georeference from .merge import merge from .polygonize import polygonize +from .post_process import post_process from .qr_code_reader import read as read_qr_code __all__ = ( - "enrich", - "clean", "clip", "detect_markings", "georeference", - "read_qr_code", - "polygonize", "merge", + "polygonize", + "post_process", + "read_qr_code", ) diff --git a/sketch_map_tool/upload_processing/detect_markings.py b/sketch_map_tool/upload_processing/detect_markings.py index 9d1d2a85..1038a715 100644 --- a/sketch_map_tool/upload_processing/detect_markings.py +++ b/sketch_map_tool/upload_processing/detect_markings.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import cv2 import numpy as np from numpy.typing import NDArray from PIL import Image @@ -11,20 +11,21 @@ def detect_markings( image: NDArray, yolo_model: YOLO, sam_predictor: SamPredictor, -) -> NDArray: - # Sam can only deal with RGB and not RGBA etc. +) -> list[NDArray]: + # SAM can only deal with RGB and not RGBA etc. img = Image.fromarray(image[:, :, ::-1]).convert("RGB") # masks represent markings - masks, colors = apply_ml_pipeline(img, yolo_model, sam_predictor) + masks, bboxes, colors = apply_ml_pipeline(img, yolo_model, sam_predictor) colors = [int(c) + 1 for c in colors] # +1 because 0 is background - return create_marking_array(masks, colors, image) + processed_markings = post_process(masks, bboxes, colors) + return processed_markings def apply_ml_pipeline( image: Image.Image, yolo_model: YOLO, sam_predictor: SamPredictor, -) -> tuple[list, list]: +) -> tuple[list, list, list]: """Apply the entire machine learning pipeline on an image. Steps: @@ -39,7 +40,7 @@ def apply_ml_pipeline( """ bounding_boxes, class_labels = apply_yolo(image, yolo_model) masks, _ = apply_sam(image, bounding_boxes, sam_predictor) - return masks, class_labels + return masks, bounding_boxes, class_labels def apply_yolo( @@ -81,7 +82,7 @@ def apply_sam( return masks, scores -def mask_from_bbox(bbox, sam_predictor: SamPredictor) -> tuple: +def mask_from_bbox(bbox: list, sam_predictor: SamPredictor) -> tuple: """Generate a mask using SAM (Segment Anything) predictor for a given bounding box. Returns: @@ -92,24 +93,51 @@ def mask_from_bbox(bbox, sam_predictor: SamPredictor) -> tuple: def create_marking_array( - masks: list[NDArray], - colors: list[int], - image: NDArray, + mask: NDArray, + color: int, ) -> NDArray: - """Create a single color marking array based on masks and colors. + """Create a single color marking array based on masks and colors.""" + single_color_marking = np.zeros(mask.shape, dtype=np.uint8) + single_color_marking[mask] = color + return single_color_marking - Parameters: - - masks: List of masks representing markings. - - colors: List of colors corresponding to each mask. - - image: Original sketch map frame. - Returns: - NDArray: Single color marking array. +def post_process( + masks: list[NDArray], + bboxes: list[list[int]], + colors, +) -> list[NDArray]: + """Post-processes masks and bounding boxes to clean-up and fill contours. + + Apply morphological operations to clean the masks, creates contours and fills them. """ - single_color_marking = np.zeros( - (image.shape[0], image.shape[1]), - dtype=np.uint8, - ) - for color, mask in zip(colors, masks): - single_color_marking[mask] = color - return single_color_marking + # Convert and preprocess masks + preprocessed_masks = np.array([np.vstack(mask) for mask in masks], dtype=np.float32) + preprocessed_masks[preprocessed_masks == 0] = np.nan + + # Calculate height and width for each bounding box + bbox_sizes = [np.array([bbox[2] - bbox[0], bbox[3] - bbox[1]]) for bbox in bboxes] + + processed_markings = [] + for i, (mask, color) in enumerate(zip(preprocessed_masks, colors)): + # Calculate kernel size as 5% of the bounding box dimensions + kernel_size = tuple((bbox_sizes[i] * 0.05).astype(int)) + kernel = np.ones(kernel_size, np.uint8) + + # Apply morphological closing operation + mask_closed = cv2.morphologyEx(mask.astype("uint8"), cv2.MORPH_CLOSE, kernel) + + # Find contours + mask_contour, _ = cv2.findContours( + mask_closed, + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE, + ) + + # Create a blank canvas for filled contours + mask_filled = np.zeros_like(mask_closed, dtype=np.uint8) + cv2.drawContours(mask_filled, mask_contour, -1, 1, thickness=cv2.FILLED) + + # Mask to markings array + processed_markings.append(create_marking_array(mask_filled.astype(bool), color)) + return processed_markings diff --git a/sketch_map_tool/upload_processing/polygonize.py b/sketch_map_tool/upload_processing/polygonize.py index 5bc2ff7c..5ca8b69b 100644 --- a/sketch_map_tool/upload_processing/polygonize.py +++ b/sketch_map_tool/upload_processing/polygonize.py @@ -4,20 +4,22 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory import geojson +from geojson import FeatureCollection from osgeo import gdal, ogr from pyproj import Transformer -def transform(feature: geojson.FeatureCollection): +def transform(feature: FeatureCollection) -> FeatureCollection: """Reproject GeoJSON from WebMercator to EPSG:4326""" transformer = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True) - return geojson.utils.map_tuples( + raw = geojson.utils.map_tuples( # type: ignore lambda coordinates: transformer.transform(coordinates[0], coordinates[1]), deepcopy(feature), ) + return geojson.loads(geojson.dumps(raw)) -def polygonize(geotiff: BytesIO, layer_name: str) -> BytesIO: +def polygonize(geotiff: BytesIO, layer_name: str) -> FeatureCollection: """Produces a polygon feature layer (GeoJSON) from a raster (GeoTIFF).""" gdal.UseExceptions() ogr.UseExceptions() @@ -47,7 +49,6 @@ def polygonize(geotiff: BytesIO, layer_name: str) -> BytesIO: src_ds = None # close dataset dst_ds = None # close dataset - with open(str(outfile_name), "rb") as f: - feature = geojson.FeatureCollection(geojson.load(f)) - feature = transform(feature) - return BytesIO(geojson.dumps(feature).encode()) + with open(outfile_name, "rb") as f: + fc = geojson.load(f) + return transform(fc) diff --git a/sketch_map_tool/upload_processing/post_process.py b/sketch_map_tool/upload_processing/post_process.py new file mode 100644 index 00000000..ad183342 --- /dev/null +++ b/sketch_map_tool/upload_processing/post_process.py @@ -0,0 +1,139 @@ +import geojson +from geojson import FeatureCollection +from shapely import MultiPolygon, Polygon +from shapely.geometry import mapping, shape +from shapely.ops import cascaded_union +from shapelysmooth import chaikin_smooth + +from sketch_map_tool.definitions import COLORS + + +def post_process(fc: FeatureCollection, name: str) -> FeatureCollection: + fc = clean(fc) + fc = enrich(fc, properties={"name": name}) + fc = simplify(fc) + fc = smooth(fc) + return fc + + +def clean(fc: FeatureCollection) -> FeatureCollection: + """Clean GeoJSON. + + Delete all polygons, which do not have the value 255 (are no markings). + Delete all inner rings inside the polygons. + """ + # f -> feature + # fc -> feature collection + fc.features = [f for f in fc.features if f.properties["color"] != "0"] + for f in fc.features: + if not isinstance(f.geometry, geojson.Polygon): + raise TypeError( + "geojson should never contain another geometry type than Polygon" + ) + f.geometry.coordinates = [f.geometry.coordinates[0]] # Delete inner ring + return fc + + +def enrich(fc: FeatureCollection, properties): + """Enrich GeoJSON properties and add color information to them.""" + for feature in fc.features: + feature.properties = feature.properties | properties + if "color" in feature.properties.keys(): + feature.properties["color"] = COLORS[feature.properties["color"]] + return fc + + +def simplify(fc: FeatureCollection) -> FeatureCollection: + """Simplifies the geometries in a GeoJSON FeatureCollection. + + Buffers each geometry based on a percentage of the maximum width, dissolves the + geometries based on the 'color' attribute, removes inner rings, and then re-applies + a negative buffer to restore the original size. + + The function assumes that the 'color' field exists in the properties of the + features. + """ + features = fc["features"] + properties = features[0]["properties"] # properties for each features are the same + geometries = [shape(feature["geometry"]) for feature in features] + + # Buffer operation + buffer_distance_percentage = 0.1 + + # TODO: does this work for bbox including antimeridian? + # (Currently SMT does not allow bbox including antimeridian as input) + max_diag = max( + ( + # bounds: (minx, miny, maxx, maxy) + (geometry.bounds[2] - geometry.bounds[0]) ** 2 + + (geometry.bounds[3] - geometry.bounds[1]) ** 2 + ) + ** 0.5 + for geometry in geometries + ) + buffer_distance = buffer_distance_percentage * max_diag + buffered_geometries = [geometry.buffer(buffer_distance) for geometry in geometries] + # Dissolve by color field (assuming there's a "color" field) + dissolved_geometries = cascaded_union(buffered_geometries) + if isinstance(dissolved_geometries, list): + dissolved_geometries = [ + remove_inner_rings(geometry) for geometry in dissolved_geometries + ] + else: + dissolved_geometries = [remove_inner_rings(dissolved_geometries)] + + simplified_geometries = [ + geometry.buffer(-buffer_distance).simplify(0.0025 * max_diag) + for geometry in dissolved_geometries + ] + + # Create a single GeoJSON feature + features = [ + geojson.Feature(geometry=mapping(geometry), properties=properties) + for geometry in simplified_geometries + ] + + # Create a GeoJSON feature collection with the single feature + fc = geojson.FeatureCollection(features) + return fc + + +def remove_inner_rings(geometry: Polygon | MultiPolygon) -> Polygon | MultiPolygon: + """Removes inner rings (holes) from a given Shapely geometry object.""" + if geometry.is_empty: + return geometry + elif geometry.type == "Polygon": + return Polygon(geometry.exterior) + elif geometry.type == "MultiPolygon": + return MultiPolygon([Polygon(poly.exterior) for poly in geometry.geoms]) + else: + raise ValueError("Unsupported geometry type") + + +def smooth(fc: FeatureCollection) -> FeatureCollection: + """Smoothens the polygon geometries in a GeoJSON FeatureCollection. + + This function applies a Chaikin smoothing algorithm to each polygon geometry in the + given FeatureCollection. Non-polygon geometries are skipped. The function updates + the geometries while retaining the properties of each feature. + """ + features = fc["features"] + updated_features = [] + + for feature in features: + geometry = feature["geometry"] + properties = feature["properties"] + + if geometry["type"] == "Polygon": + geometry = Polygon(geometry["coordinates"][0]) # Exterior ring + else: + continue # Skip non-polygon geometries + + corrected_geometry = chaikin_smooth(geometry) + + updated_features.append( + geojson.Feature(geometry=mapping(corrected_geometry), properties=properties) + ) + + fc = geojson.FeatureCollection(updated_features) + return fc diff --git a/tests/integration/upload_processing/test_detect_markings.py b/tests/integration/upload_processing/test_detect_markings.py index 894cc320..21d0288b 100644 --- a/tests/integration/upload_processing/test_detect_markings.py +++ b/tests/integration/upload_processing/test_detect_markings.py @@ -38,9 +38,11 @@ def test_detect_markings(sam_predictor, yolo_model, map_frame_marked): def test_apply_ml_pipeline(sam_predictor, yolo_model, map_frame_marked): - masks, colors = apply_ml_pipeline(map_frame_marked, yolo_model, sam_predictor) + masks, bboxes, colors = apply_ml_pipeline( + map_frame_marked, yolo_model, sam_predictor + ) # TODO: Should the len not be 2? Only two markings are on the input image. - assert len(masks) == len(colors) == 6 + assert len(masks) == len(colors) @pytest.mark.skip("For manuel testing") @@ -49,7 +51,7 @@ def test_apply_ml_pipeline_show_masks( yolo_model, map_frame_marked, ): - masks, _ = apply_ml_pipeline(map_frame_marked, yolo_model, sam_predictor) + masks, _, _ = apply_ml_pipeline(map_frame_marked, yolo_model, sam_predictor) for mask in masks: plt.imshow(mask, cmap="viridis", alpha=0.7) plt.show() diff --git a/tests/unit/upload_processing/test_clean.py b/tests/unit/upload_processing/test_clean.py deleted file mode 100644 index 063ed406..00000000 --- a/tests/unit/upload_processing/test_clean.py +++ /dev/null @@ -1,8 +0,0 @@ -from geojson import FeatureCollection - -from sketch_map_tool.upload_processing import clean - - -def test_clean(detected_markings): - result = clean(detected_markings) - assert isinstance(result, FeatureCollection) diff --git a/tests/unit/upload_processing/test_polygonize.py b/tests/unit/upload_processing/test_polygonize.py index 99c3d8b1..b92736e6 100644 --- a/tests/unit/upload_processing/test_polygonize.py +++ b/tests/unit/upload_processing/test_polygonize.py @@ -1,12 +1,9 @@ -from io import BytesIO - -import geojson +from geojson import FeatureCollection from sketch_map_tool.upload_processing import polygonize def test_polygonize(sketch_map_frame_markings_detected_buffer): - geojson_buffer = polygonize(sketch_map_frame_markings_detected_buffer, "red") - json = geojson.loads(geojson_buffer.read()) - assert json.is_valid - assert isinstance(geojson_buffer, BytesIO) + fc = polygonize(sketch_map_frame_markings_detected_buffer, "red") + assert fc.is_valid + assert isinstance(fc, FeatureCollection) diff --git a/tests/unit/upload_processing/test_enrich.py b/tests/unit/upload_processing/test_post_process.py similarity index 53% rename from tests/unit/upload_processing/test_enrich.py rename to tests/unit/upload_processing/test_post_process.py index d87195da..3249e8e7 100644 --- a/tests/unit/upload_processing/test_enrich.py +++ b/tests/unit/upload_processing/test_post_process.py @@ -1,4 +1,11 @@ -from sketch_map_tool.upload_processing import enrich +from geojson import FeatureCollection + +from sketch_map_tool.upload_processing.post_process import clean, enrich + + +def test_clean(detected_markings): + result = clean(detected_markings) + assert isinstance(result, FeatureCollection) def test_enrich(detected_markings_cleaned):