From 7ce3fe722b78d0faf07662fafb81c78376a40203 Mon Sep 17 00:00:00 2001 From: nghiadang Date: Wed, 28 Aug 2024 05:10:02 +0000 Subject: [PATCH] first commit --- ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.dbf | Bin 0 -> 445486 bytes ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.prj | 1 + ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.qpj | 1 + ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shp | Bin 0 -> 424656 bytes ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shx | Bin 0 -> 9156 bytes ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.dbf | Bin 0 -> 445486 bytes ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.prj | 1 + ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.qpj | 1 + ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.shp | Bin 0 -> 423264 bytes ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.shx | Bin 0 -> 9156 bytes .../region/ST_ThuanHoa_Boundaryofficially.cpg | 1 + .../region/ST_ThuanHoa_Boundaryofficially.dbf | Bin 0 -> 1003 bytes .../region/ST_ThuanHoa_Boundaryofficially.prj | 1 + .../region/ST_ThuanHoa_Boundaryofficially.qmd | 27 + .../region/ST_ThuanHoa_Boundaryofficially.shp | Bin 0 -> 13500 bytes .../region/ST_ThuanHoa_Boundaryofficially.shx | Bin 0 -> 108 bytes deafrica_tools/__init__.py | 26 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 687 bytes .../__pycache__/bandindices.cpython-310.pyc | Bin 0 -> 19159 bytes deafrica_tools/app/__init__.py | 0 deafrica_tools/app/animations.py | 942 ++++++++++ deafrica_tools/app/changefilmstrips.py | 275 +++ deafrica_tools/app/crophealth.py | 337 ++++ deafrica_tools/app/deacoastlines.py | 505 +++++ deafrica_tools/app/forestmonitoring.py | 1026 ++++++++++ deafrica_tools/app/geomedian.py | 126 ++ deafrica_tools/app/imageexport.py | 372 ++++ deafrica_tools/app/wetlandsinsighttool.py | 388 ++++ deafrica_tools/app/widgetconstructors.py | 367 ++++ deafrica_tools/areaofinterest.py | 54 + deafrica_tools/bandindices.py | 615 ++++++ deafrica_tools/classification.py | 1674 +++++++++++++++++ deafrica_tools/coastal.py | 1123 +++++++++++ deafrica_tools/dask.py | 106 ++ deafrica_tools/datahandling.py | 1523 +++++++++++++++ deafrica_tools/load_era5.py | 411 ++++ deafrica_tools/load_isda.py | 92 + deafrica_tools/load_soil_moisture.py | 39 + .../locales/fr/LC_MESSAGES/deafrica_tools.mo | Bin 0 -> 1783 bytes .../locales/fr/LC_MESSAGES/deafrica_tools.po | 117 ++ deafrica_tools/plotting.py | 1272 +++++++++++++ deafrica_tools/spatial.py | 949 ++++++++++ deafrica_tools/temporal.py | 576 ++++++ deafrica_tools/untitled.txt | 0 deafrica_tools/wetlands.py | 732 +++++++ new_import.py | 320 ++++ region/ST_region.dbf | Bin 0 -> 735 bytes region/ST_region.prj | 1 + region/ST_region.shp | Bin 0 -> 82428 bytes region/ST_region.shx | Bin 0 -> 108 bytes train/ST_training data_updated_1130points.dbf | Bin 0 -> 478216 bytes train/ST_training data_updated_1130points.prj | 1 + train/ST_training data_updated_1130points.qml | 835 ++++++++ train/ST_training data_updated_1130points.qpj | 1 + train/ST_training data_updated_1130points.shp | Bin 0 -> 31740 bytes train/ST_training data_updated_1130points.shx | Bin 0 -> 9140 bytes utils.py | 6 + 57 files changed, 14844 insertions(+) create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.dbf create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.prj create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.qpj create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shp create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shx create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.dbf create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.prj create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.qpj create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.shp create mode 100644 ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.shx create mode 100644 ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.cpg create mode 100644 ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.dbf create mode 100644 ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.prj create mode 100644 ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.qmd create mode 100644 ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.shp create mode 100644 ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.shx create mode 100644 deafrica_tools/__init__.py create mode 100644 deafrica_tools/__pycache__/__init__.cpython-310.pyc create mode 100644 deafrica_tools/__pycache__/bandindices.cpython-310.pyc create mode 100644 deafrica_tools/app/__init__.py create mode 100644 deafrica_tools/app/animations.py create mode 100644 deafrica_tools/app/changefilmstrips.py create mode 100644 deafrica_tools/app/crophealth.py create mode 100644 deafrica_tools/app/deacoastlines.py create mode 100644 deafrica_tools/app/forestmonitoring.py create mode 100644 deafrica_tools/app/geomedian.py create mode 100644 deafrica_tools/app/imageexport.py create mode 100644 deafrica_tools/app/wetlandsinsighttool.py create mode 100644 deafrica_tools/app/widgetconstructors.py create mode 100644 deafrica_tools/areaofinterest.py create mode 100644 deafrica_tools/bandindices.py create mode 100644 deafrica_tools/classification.py create mode 100644 deafrica_tools/coastal.py create mode 100644 deafrica_tools/dask.py create mode 100644 deafrica_tools/datahandling.py create mode 100644 deafrica_tools/load_era5.py create mode 100644 deafrica_tools/load_isda.py create mode 100644 deafrica_tools/load_soil_moisture.py create mode 100644 deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.mo create mode 100644 deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.po create mode 100644 deafrica_tools/plotting.py create mode 100644 deafrica_tools/spatial.py create mode 100644 deafrica_tools/temporal.py create mode 100644 deafrica_tools/untitled.txt create mode 100644 deafrica_tools/wetlands.py create mode 100644 new_import.py create mode 100644 region/ST_region.dbf create mode 100644 region/ST_region.prj create mode 100644 region/ST_region.shp create mode 100644 region/ST_region.shx create mode 100644 train/ST_training data_updated_1130points.dbf create mode 100644 train/ST_training data_updated_1130points.prj create mode 100644 train/ST_training data_updated_1130points.qml create mode 100644 train/ST_training data_updated_1130points.qpj create mode 100644 train/ST_training data_updated_1130points.shp create mode 100644 train/ST_training data_updated_1130points.shx create mode 100644 utils.py diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.dbf b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.dbf new file mode 100644 index 0000000000000000000000000000000000000000..a806535f1529b615cc59b20c6848dd600bd20bc1 GIT binary patch literal 445486 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 literal 0 HcmV?d00001 diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.prj b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.prj new file mode 100644 index 0000000..39dec21 --- /dev/null +++ b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.prj @@ -0,0 +1 @@ +PROJCS["Transverse_Mercator",GEOGCS["GCS_WGS_1984",DATUM["D_unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] \ No newline at end of file diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.qpj b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.qpj new file mode 100644 index 0000000..8a1d453 --- /dev/null +++ b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.qpj @@ -0,0 +1 @@ +PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563],TOWGS84[-192.873,-39.382,-111.202,-0.00205,-0.0005,0.00335,0.0188]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shp b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shp new file mode 100644 index 0000000000000000000000000000000000000000..80519f5ef75d565d01a68fda0b7d4e671506faca GIT binary patch literal 424656 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 z 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 J%R8s{{{V4~17QFF literal 0 HcmV?d00001 diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shx b/ThuanHoa/KhoanhDat/ThuanHoa_KKDD2019.shx new file mode 100644 index 0000000000000000000000000000000000000000..256ef801e1d178b68ad7890a9c59cd53005b3a1a GIT binary patch literal 9156 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 literal 0 HcmV?d00001 diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.prj b/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.prj new file mode 100644 index 0000000..39dec21 --- /dev/null +++ b/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.prj @@ -0,0 +1 @@ +PROJCS["Transverse_Mercator",GEOGCS["GCS_WGS_1984",DATUM["D_unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] \ No newline at end of file diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.qpj b/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.qpj new file mode 100644 index 0000000..8a1d453 --- /dev/null +++ b/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.qpj @@ -0,0 +1 @@ +PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563],TOWGS84[-192.873,-39.382,-111.202,-0.00205,-0.0005,0.00335,0.0188]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] diff --git a/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.shp b/ThuanHoa/KhoanhDat/ThuanHoa_TKDD2022.shp new file mode 100644 index 0000000000000000000000000000000000000000..0bc61ec94f0449f0f272ed0f1df40902377bc1ef GIT binary patch literal 423264 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 Ot+I0 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⪚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 OpXmuSeP-mtivI(Kx&Ukd literal 0 HcmV?d00001 diff --git a/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.cpg b/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.cpg new file mode 100644 index 0000000..3ad133c --- /dev/null +++ b/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.dbf b/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.dbf new file mode 100644 index 0000000000000000000000000000000000000000..cb95cc3a678179785cc40331fbf56cbb4d42fede GIT binary patch literal 1003 zcmdUry-ve05XVh}C@Mni49Ihb%8f}2bH_?7^aYGv$reeBuq!d}0=yZIg?+Uh>C&Mq zxA^@3-FNj}O)m%`V)7H8kr`E_m*`!W@6qx4AW{tQ8^pgdB?R~sai`xzuWVF-S0f%f zfC&5wab@v}^G>ujj3|8Se~Ie9>%un?%@9{cDAC9E1o#8y-sKCz z{1e3ezL)v{BIMVIhx$^Rf&U`jDW_`2c#xs~c>YAwe0MnCN!!}bsGgk9>3DxHx|BcK z-+OH(EO7EZw=@}}biHE19-ld-eA%p5lm?N)pVEdkPm4MSF9K;IJ + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + diff --git a/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.shp b/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.shp new file mode 100644 index 0000000000000000000000000000000000000000..b003a6434a0dad9449617eb9d0a26ad8bf9fd12c GIT binary patch literal 13500 zcma*ud0fr;+sE;2*>^L>+L3jvA)_pD>}AU`WJ`7``%)AsOGHws?2*cDEJ?Bq#b7LB z&63dA$w+p|+^_dJ?#F%KkH_zid;alw_C4pjT%YT6eXh^9#=@d*F^m8BUuD;41uQHI zaqMaC_BYu+24m{`dg~uJ+89q8_U=D=*kHVU>EqiALv4)z@$3KhhxsflD%bn}oQn@y zbl|%B?^C~rV@fPG7$=uoa3Zgtjj?HwT|;jzG8p%C&8++k-|&7Oe-7_>bG6Y6{4Vdh z_ZiInO_PU)q_jxEBg?o{dWoB4)LNW`S5GR}F$@1ZG$ellPIfHuw>&=d)zJxf(aSzR zeZ&=cz>#?4>#dfbaORu}jr(G*|K$PhmN{qD(}e~-|A}locI?p0*=!|~P z@%`Sx6(0wfo~KKJUv2cghpD*JGS6{g9M`}1{0&Af^CbpPA0Ij~54$Ei?)r**v|Q?6 z9q-@pdR$q)@Amle-xVk?z`(AtoyHwmm1v)9m0xRU0;K-wx$0MX_)gmmcWr6mo2}I|2wb!BRUq(z>Is+ zAdX+_IkHUA0X9ajn{|A+JLdCF%`o?UKE>=E12FmP`vNnr3&!A#T9zl18Gptn%w~{{G4RLRrTUS7 zy8cwGIH%#@s7X0HFypbh9r+u-?$m=@IOl3l>!pDPN0AA#4dSu@CP`Tx`j^1bXp%y?Ei#_&VX zb$>~ie6k*m$+HoOn0%<_fotrYc6LA3d<@3q_4wVG{2#p!>;87&YJ*4T+6J5A=Ny9> z$CV#3`Fv+B_NlNZu-FQNv1a<`Z)@-}&!;{eG2`VCh4p-aSa}eRnJ-b(LQM7N!D38& z+mu{sP+b0=hp9(*>tgE5Y(LC-x{t!KQ`-JHA2Uun;xPHz(;q9|S1|b$ER#nC`-htH zF-zwoE*Zx$^XtrP%y|!g#chk-+%gOE@w7H!rhFSX12ayW{4nRKH3{>at%5LhVxuSK z-|8D8v^pXh;!?~>}6b>!$& z%)Id$gDX%_L$GsjRAbwy+s`vF@yzRviTiawto3XGW}F8s$GfTyj*Y~cuPZU}-MSx>4+|o3rGf)4UBlFi zl^ZbYS;`koejkr9A74=^+Mws28i$pS%`o}>DIV+ky)f(5&aGJM$vCX_bsOe6Y6N1{ zF@2u;-XI(^UMF=vmD6>2@aWc7vhx2TrvB8_^%;)0_b~UHx(O4Hs$VeU(S9Q)KXQt0 zGUfBlwV3?${0VEl*YhzxlN(?^_oLS%uT@hke4BT@09JiIjlXVxlao!oBtAVZVDjR57N!ma-^J9yHyK#-`x&P0Rqo>|72>dPx1%sk91ff+C3G<-a|auX{| zeqS1c8Q&32F!6pc9H0DibLXyDb$c*YUGv21^ZH}z!O0}7y6B9_r~2ubI#^X!-Fb_t z|J!B7qv{q@ecjm?tG}4r_c81EruLZY-mVmDif08oOx}zdh5J8@8`2O{ z2OfA~?!Q-ktnprfH6P@@B=Qxkc~lS2eqLvvG0rqU|FXu!^WGG!_0)h>4=-ct%+P9> zJn9q}Z|axI7sKSc#~rM7G#@5^?{?m5s+aa3Ss$tM#jfGet?y5Mgh|_C-!SXWlm}RS zV&iS*{FS+0)@rQv<0a;PHl||g!n0SH+xK{h$%m0ySp7k{?WX)|`4p?)al+)COFCAc z=7pK>4p*`A>kZa8Ct=k~`yHnBrQvqW_@4H~s*9_!=5H+K^C7`l_4gDeZ>lZDs+*57 z>qX*X%zU11vD3U>$>j9}E6n+7Ey0Y>rZ!mhI0$pU79+9hVHjqd9xlR+^Wn8vKVOR# zmkpTlY`G0<{%*v~hsP(d`tNA0`_IOTi#|`C{IT*bb3ZRrC*JkO-2VohpY#Y1!t}#Q zkyz{XT}&PxlR19PZnwGqM`7jXBFz2g={WH$c>**4Kk7LDR?NbTU(6azy#BG;W6I}) zvih4TSbeDCOW)ru32WWg`3QH1b69$DQh1BzJK2it8eqgrln?>Sj!|5h2RUT0(C*H$JTp&v1Enp6pM zy}8Br8Z=MiWQ|{K%=IjFKGw5*73^kvFfP{*8Zy0KJ)rG3^QI!YGKxe0>iP^r&d_~ zoUD4}fwd00V&&_6TyQ|CSNcAue;tqM?=I@N?)L~L&ogzLde`w0RvdMG_JOm%V)C$g zf2{dZX1}?A?u}K~J7MZ~jV_qyJ?MtXb6X>(Eu7+xxu5PDPg8s_;~609cmSrpjMwKC zw@6GpH+I6rx9I___;<&Qcg|@X(>bxZGv@2Zu3-8L=K)yfd4tK{8e_5IP~m{7e)@W2 zt#j2d!`M>!NL(h?#ad72eRp%Lb-@qw^8!woK5(EvR$nz2GfrO?VD$?dF!4LN2$Sb7 z$1(MLdmz@nD;+aWs;FLRoL*wh6IpTpfQd(p>XYhC$n%z{>dHl z+!+(G?r#sKU3fGYGj3fnu=+7a%sz5gHs(CG9kAAqG6zlfYixlz-M=lc#;+>YdhLq& zd_o@kGwMyXMOfFnfI07v;g~otNyJ*mc4GR0cJY|;*`JP?H?txzbt(O|lcAEups z8i4t{-$Z=z!i5VPFnJs{606TUjLE}M1F`b)F(&Sjj+lJ*Ds#xxpY&;q)ekqs%A1y$ ze6s0=*?&FL{!{&;8)n|rYij;{m=9*0l5DWz7l-Ln8#l*_a{|_RY_YC)4wDb}TVV2f z#vRN)^^zS{zhjYPFs!*5t;*mNUvyRR0fte@Aqp|Y0C#J7){0kG$W;(9*DL?PQSPxRV zVfF!~oiX$2i34U@89gxT-RxFa>xCDlE?!YQ)sOpP?GN?*)YT9_%(`n(eCZ>c=U}b# zjWKyRdM2i>SvJJvP2LR5yg%CjE6?09>)h9xSaBGIRj;dJK3Bxe{Qa@zuuY?<4ecou7H|M-EmWGZnM`$3DlZ^ZuBBa_(TpH98#Ahc!Kq zwcbQyu2cJW%sN+Q=lAdL!}Kxp4q)|@+pywy7%N`USoyE_j?||EQCRPxWafL=3aosS z)ju!){{3W3cz*H6y6;3xzj0w2R{y5!X}*lbtkdWAVA7-FaLj#Ajm3O!M_W*a}d_PpaItVh~8NBbNwwfvFcGG^M!d>s|{9vVPrltPYMsfdaw2a*1pCCt53+H zez1;2$khL-*D!VZ;y|qTV8<|h&shk%kuP-Y;X`9v=8Lr+{DgVT zE;_&JZUyu4{d+KZz3mn6)5xclH!$ndh#Q!6s&CKp(>Fd2#^h(?WK2HwX@V8cCz$a% zR1vE_mn5Fre_CLzH>!WC8!vc2sQb0U>=)MG!_=FJ?XdFiI@WvE_L%uz=>jI79F3Sd zRU{R&Z;Nvq(&vjPsQ?;KOfXRTD znuO`+wnSpR7a4~cud7Qi^Y^nG=6OH+Vd_U}Urc=+JJJ04ZyhlGbn{_Y>uEF0cs}cm z$&>an^J%86@sn9^hRM8+d2fli-C?b;_RnebPt=hvwJ~wBnuGN|q6F6bEsC{X>Gug* zUmnsQQeVnG#aws%IjnW=-uK@h?84e_UdNQ5Z(*1`JDrND4~yqv#%s?mth|_t8OOKF zvHCwxO!$)LV9oDQn7%&Q73=peeK70Di8h#z6)l2!j?624O~)VS(od^CoW$hG=a*RT z2X|rWcfO~XG|3l_HD4ZL>X}yzWpVe+X;cn2V_+QQNWB z4KJ+y&RWboxikrD{AOe2*C?#|*$k`y=!)r=ZoOhZ!u1a|#Eegic+C7dUmR;5&&NE@ zicfva@BjK?#;5XQtbWvpweH=+if;+5_bX>H@v5!;8|B6B6xQ`~+25&toW$gD=d)P- zMJ(n%Zf?Wm^VLvHKl?EZD{tmwjsHTd{GEvj$6r2J{edUe{G5)-yVoPI`h>xl`B>e- z{QWpbT-(xrrZv|4xfYnb`!^p}-(MV458oVjHrFGW>-64%^?o;r{VsK9!F;TK$qQ?L z<%SveO3s*gUFwGwpZb{b?9myMrfuqCt!EuD>D8$YW?y6LfVF?qapjX8W;{J)=Gk(Y zyx-RVE5Djz?kmI|D_&NZzGO-VtUCT9CT#VbFxOdF5-T6NW7d(YCCpyniK!zSzV$Yb zxAyzmM`U60>hclHxU{^C>333!5)b9cI?TK&|A6CM_i`v!|Cx>{57$;=`nZd`zn?D< zYd^9IGi~<;VCvf5#hCTut{*0E|8c|2i`}y^<2=d=Q!nzSV7?dnjQ17lf7~(uhCamP zUw}+pKP6-Jd*iV3Ed;9`kHtLBNPkS7jnr}Vv$HVIm45;z{_jU%){}D{SnEX(tUhZh zrk_c)$K=lqZ_IpMR3EF4Ux*nu|5}(IzYN0E-z8-+>)G!cG5zw=T;A94yhjgW>iguE zn0fvC8O(ijxP^JHrWsi0J%bgu*I4qD#G5urRc9_pub-|oJt_s$BvA&o-DsVmTzvve|y|C)n0<8Yc zAFIxLV%=X5)_$WG*7_8VwJum;=AFkT%yptmW9Hr1Sgd^c!uvY)Lt8QDsd68yPuqr> zo+oc(#`Cz$v@CfU>pt{(#^u*ktbRBStKUk({QQR)%=b3#!_1q~(Pmdqz{EKq0;|uF z8TU$yG0%13AZ9*w8;-fYee|8PbeD~G$_$Rs|)_brG znDzOdCD!}lFwF8=NApj8=~B$Rn*ISRUzT9~{_#C#{x=6D-z|_^#zhL$A(U^Hxvmw^FFT~`Xw*jl) z7>Tt$RK)z;t|R7s=&Z6>^P~n=Jt~g*T=#!?|EoN*#Ekd-i&*2D534R6#)?O7FH=91 z5R27s^#74|8 z8INJ@3*sIhgypvI47*%f+fU3$f~L0iK`FZS}^S z_xD1WI5u?0T(7Go*88M!m^|865HnnN24mLswqH4)-v9Q%8uu(rJ9MfoRvfNly}xXX z)t4Q^Oux4j$;&0*6lHAn0fPeG}ii^iq(%F!t|A{aailbar1G*YOMNj0u$GF z!B}-d);KQ2I^R*u{;Q-nCQpYXVe06!Nm%{iLCj-!@xavS345{n)iIcHcZ$c9sR6E7 z>){$qzw%`OR{hiS(x3SE!-`J;)_%MMp%;OZijd`wgTdcb9oB8)AZ|h>kBNA(UwZin}+k>$7gH^ER%R)@waI`#D zJ@mm`$FmsLy60v7{yV zGkc5TrTJom>C4=8J=LQ+SpDEM%seep1=FuuPR3l{vpgpMtQAkKOGPmEd(suNUs&<2 zyN%}U^gfunfBp^T{5v{gp7X>L%z3g}VB+@SF4jKD8j}~bu42y9xH2Z+-7aFiulWI! z2TT9Ntc%M^VDdZu6lUCB6~cOda2zXMpZWcq>g)l`IE;9WRcCf!;{5&zW<4w!gBi!* zN0|Gwh{oi1>wB2z`!^EP7d*U!8Q*0qz8}AdH9qq(&yjWm>;1<}O#jtKreA130aJJT z$~v#i<;Tm!HGVwi{x;vj^kLq8vHE>opE{e}7E_n@WMc07*V-k?`?R(y0;v4$}6YlB%V(o*kW9r55(!`H>TkH?4KB7Ek{Y*>2+OJf{ z?BkXuW7Wq7SoyOXbN@A(VXa$nSo`Io;tR9`m{>v~_X z@@p(+U3PhgwLTBR+}F8RW+&->SqH73VV<*=1LpqPJ;ba>_3SbAqs-q}?|bz8gyo6; z&XIMkYa^_>BWr(H5A$!1tnsgdb)HPDzbC1Q=`Y5}>YuA&y(iH1wBA+1Ja3i9nCCrR z0rR}ao?_M*xl}|U!K5UQ+*7cpR z`UFR;{ZemCo%gDOSzme$!PNPRk77;dvzv&C^WObf>y0mF-OdQbtOFiPvGSlAW<8sA z6xXJ|&5zmd#GJ!guix|c0;c^Lrf+DNg*6_^f9~h~Jxn~?W?=f2@_%9V3D2?K&s@UH zx1pL(oc~4?Rz85`$CICe5(wyC|I z^7jv{&xch{^gd)iCa;n24MQ*@MBp0 z>nyCk?GMa&tog{_e^75$m!W>^{o75feM?PDzwq1_>;2LYtobnwlaKR9V)E;VCuV*& z7>ikVOAN#6x2Ivo|4lcn=bVexXV_x;vb%v;^|BJy^RC2L6=h0YwTsmg{#d%`t?7{PxasO>9)_a6#taWf6<~>9OcdR}+1jEM8nEUdNz}okv z^Y>xuxAtSz-!#lT?3aKUU;8ku_?^a@&l529<@bx2@#-=TtDn4t=@T7f_BlVx-0!kN zn0a6M9#(!_J}L zSnG8OO#DJ~`1?B5S!=9%`a9-%jma6_~ zad{Ah$+LGd@$>D7$=`{ISo>BZW}aPimB7DFJRW`Q)jX2y&qth@ zkM&b9r@14Ohs!Ep;#TDvRvjM`Y5IM?Lps*)`y4R+VYv*fekv)#)DNA1g!Q}uSpCTh ztol0)(+9i1#`GEe9kBLAA28z@QW0w&=VyP*{Z5vdwjQ$bw=mW^S{SSUNm*rjUpBoO zX8qh2i&cl~W6hT!tiHPiRvq)idam}E>twoO#&uXH%zf1Ei8UV`F?o5-0@FvWb;Y`$ zjBr!`esVnKJjM57?f++D?kjK}CQTUxQ^>zuS9_(_*jK`x8 zOqrbLf|<|H*J9OcC#-!(99BNI!o+>xZmj8 z$4|%fYbzgM^7XPe);{$XR^9i(`u*h*togbG>v`s3>duUKto&|;iRYqoSbc9PtU7le zt6$6vG1c$ZZ_M9+z6Gnk7Uuo4=350!zw+81QwR2JUt!+AxMJ-q7h}e?p9j|dp);nx zSmBG64{fmWbRO1vSPHZL=LKSoLuRmP|366Y*Hy|33)_o%&NltA z?&C04KieFuUSGgG-zNiRKF>(Qiu2Ru=JzxDeSzlP0nGcMUn=tZ1YKtc*812UYaPy6 zW`3Wh-&d&5K8DqwEyU`-T`+n2ato#&bW8~{)qjWUn0?eH1J-`WlHbSZ_cTqGn&;22 znCpe#Sz?M;`DvJW@iqW!e-ezj?^S~WP4j!^YfQbW))Q+Vl8M>pSaiqKvyJyLUrij=?_r%RZ0X*y=HsUEd{gvqmPE6n$uUSX}% kRWb1{rQdh)`Kdo*!qn+K=62?n#_A7pvFgtM{{Mmg3j?TIl>h($ literal 0 HcmV?d00001 diff --git a/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.shx b/ThuanHoa/region/ST_ThuanHoa_Boundaryofficially.shx new file mode 100644 index 0000000000000000000000000000000000000000..471d202707d650acf51b7b140d468035fdb0f4f4 GIT binary patch literal 108 zcmZQzQ0HR64$NLKGcd3M<(~gu`SC-wg5%+@8gm|a8aN(R*sy8OP6fxC=e~bF6Jy|r NT|0`X5d(vi1^}N}6mb9m literal 0 HcmV?d00001 diff --git a/deafrica_tools/__init__.py b/deafrica_tools/__init__.py new file mode 100644 index 0000000..610e60e --- /dev/null +++ b/deafrica_tools/__init__.py @@ -0,0 +1,26 @@ +__locales__ = __path__[0] + '/locales' + + +def set_lang(lang=None): + if lang is None: + import os + os_lang = os.getenv('LANG') + + # Just take the first 2 letters: 'fr' not 'fr_FR.UTF-8' + if os_lang is not None and len(os_lang) >=2: + lang = [os_lang[:2]] + else: + lang = [lang] + + import gettext + try: + translation = gettext.translation( + 'deafrica_tools', + localedir=__locales__, + languages=lang, + fallback=True + ) + translation.install() + + except FileNotFoundError: + print(f'Could not load lang={lang}') diff --git a/deafrica_tools/__pycache__/__init__.cpython-310.pyc b/deafrica_tools/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95a646e0a531c7ae574683727b230a63545d806b GIT binary patch literal 687 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 RnI9&8fxh~l_-@C4{5MKs%a8y7 literal 0 HcmV?d00001 diff --git a/deafrica_tools/app/__init__.py b/deafrica_tools/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deafrica_tools/app/animations.py b/deafrica_tools/app/animations.py new file mode 100644 index 0000000..a0ad4fb --- /dev/null +++ b/deafrica_tools/app/animations.py @@ -0,0 +1,942 @@ +# -*- coding: utf-8 -*- +""" +Satellite imagery animation widget, which can be used to interactively +produce animations for multiple DE Africa products. +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import fiona +import sys +import datacube +import warnings +import matplotlib.pyplot as plt +from datacube.utils.geometry import CRS +from ipyleaflet import ( + WMSLayer, + basemaps, + basemap_to_tiles, + Map, + DrawControl, + WidgetControl, + LayerGroup, + LayersControl, + GeoData, +) +from traitlets import Unicode +from ipywidgets import ( + GridspecLayout, + Button, + Layout, + HBox, + VBox, + HTML, + Output, +) +import json +import itertools +import numpy as np +import geopandas as gpd +from io import BytesIO +import ipywidgets as widgets +import datetime +from skimage import exposure +from skimage.filters import unsharp_mask + +from datacube.utils import masking +from datacube.utils.geometry import Geometry +from datacube.utils.masking import mask_invalid_data +import deafrica_tools.app.widgetconstructors as deawidgets +from deafrica_tools.dask import create_local_dask_cluster +from deafrica_tools.spatial import reverse_geocode +from deafrica_tools.datahandling import pan_sharpen_brovey + +import warnings + +warnings.filterwarnings("ignore") + + +# WMS params and satellite style bands +sat_params = { + "Landsat": { + "products": ["ls5_sr", "ls7_sr", "ls8_sr", "ls9_sr"], + "styles": { + "True colour": ("true_colour", ["red", "green", "blue"]), + "False colour": ( + "false_colour", + ["swir_1", "nir", "green"], + ), + }, + }, + "Sentinel-2": { + "products": ["s2_l2a"], + "styles": { + "True colour": ("simple_rgb", ["red", "green", "blue"]), + "False colour": ( + "infrared_green", + ["swir_2", "nir_1", "green"], + ), + }, + }, +} + + +def make_box_layout(): + return Layout( + # border='solid 1px black', + margin="0px 10px 10px 0px", + padding="5px 5px 5px 5px", + width="100%", + height="100%", + ) + + +def create_expanded_button(description, button_style): + return Button( + description=description, + button_style=button_style, + layout=Layout(width="auto", height="auto"), + ) + + +def update_map_layers(self): + """ + Updates map to add new DE Africa layers, styles or basemap when selected + using menu options. Triggers data reload by resetting load params + and output arrays. + """ + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Clear all layers and add basemap + self.map_layers.clear_layers() + self.map_layers.add_layer(self.basemap) + + +def extract_data(self): + + # Connect to datacube database + dc = datacube.Datacube(app="Exporting satellite images") + + # Configure local dask cluster + client = create_local_dask_cluster(return_client=True, display_client=True) + + # Convert to geopolygon + geopolygon = Geometry(geom=self.gdf_drawn.geometry[0], crs=self.gdf_drawn.crs) + + # Create query. + start_date = np.datetime64(self.start_date) + end_date = np.datetime64(self.end_date) + + self.query_params = { + "time": (str(start_date), str(end_date)), + "geopolygon": geopolygon, + } + + # Find matching datasets + dss = [ + dc.find_datasets(product=i, **self.query_params) + for i in sat_params[self.dealayer]["products"] + ] + dss = list(itertools.chain.from_iterable(dss)) + + # If data is found + if len(dss) > 0: + + # Get CRS + crs = str(dss[0].crs) + + self.load_params = { + "measurements": sat_params[self.dealayer]["styles"][self.style][1], + "resolution": (-self.resolution, self.resolution), + "output_crs": crs, + "group_by": "solar_day", + "dask_chunks": {"time": 1, "x": 2048, "y": 2048}, + "resampling": {"*": "cubic", "oa_fmask": "nearest", "fmask": "nearest"}, + } + + # Load data + from deafrica_tools.datahandling import load_ard + + timeseries_ds = load_ard( + dc=dc, + products=sat_params[self.dealayer]["products"], + min_gooddata=1.0 - (self.max_cloud_cover / 100), + ls7_slc_off=False, + mask_pixel_quality=self.cloud_mask, + **self.load_params, + **self.query_params, + ) + + # Set invalid nodata pixels to NaN + timeseries_ds = mask_invalid_data(timeseries_ds) + + # Else if no data is returned, return None + else: + timeseries_ds = None + + # Close down the dask client + client.close() + + return timeseries_ds.compute() + + +def plot_data(self, fname): + + # Data to plot + to_plot = self.timeseries_ds + + # If rolling median specified + if self.rolling_median: + with self.status_info: + print( + f"\nApplying rolling median ({self.rolling_median_window} timesteps window)" + ) + to_plot = to_plot.rolling( + time=int(self.rolling_median_window), center=True, min_periods=1 + ).median() + + # If resampling freq specified + if self.resample_freq: + with self.status_info: + print(f"\nResampling data to {self.resample_freq} frequency") + to_plot = to_plot.resample(time=self.resample_freq).median() + + # Raise by power to dampen bright features and enhance dark. + # Raise vmin and vmax by same amount to ensure proper stretch + if self.power < 1.0: + with self.status_info: + print(f"\nApplying power transformation ({self.power})") + to_plot = to_plot ** self.power + + # Apply unsharp masking to enhance overall dynamic range, + # and improve fine scale detail + if self.unsharp_mask: + with self.status_info: + print( + f"\nApplying unsharp masking with {self.unsharp_mask_radius} " + f"radius and {self.unsharp_mask_amount} amount" + ) + from skimage.exposure import rescale_intensity + + funcs_list = [ + rescale_intensity, + lambda x: unsharp_mask( + x, radius=self.unsharp_mask_radius, amount=self.unsharp_mask_amount + ), + ] + else: + funcs_list = None + + from deafrica_tools.plotting import xr_animation + + xr_animation( + output_path=fname, + ds=to_plot.dropna(dim="time", how="all"), + show_text="", + bands=sat_params[self.dealayer]["styles"][self.style][1], + interval=self.interval, + width_pixels=self.width, + show_gdf=deacoastlines_overlay(to_plot) if self.deacoastlines else None, + gdf_kwargs={"linewidth": 3}, + percentile_stretch=(self.vmin, self.vmax), + image_proc_funcs=funcs_list, + show_date="%Y" if self.resample_freq == "1Y" else "%b %Y", + annotation_kwargs={"fontsize": 75}, + ) + + # Add plot preview below map and finish + plt.show() + with self.status_info: + print(f"\nImage successfully exported to:\n{fname}.") + + +def deacoastlines_overlay(ds): + + import geopandas as gpd + import pandas as pd + import matplotlib + from shapely.geometry import box, Point + from deafrica_tools.coastal import get_coastlines + + # Get bounding box of data + xmin, ymin, xmax, ymax = ds.geobox.geographic_extent.boundingbox + bounds = [xmin, ymin, xmax, ymax] + + # Load data + deacl_gdf = get_coastlines(bbox=bounds) + + # Clip to extent of satellite data + bbox = gpd.GeoDataFrame(geometry=[ds.geobox.extent.geom], crs=ds.geobox.crs) + deacl_gdf = gpd.overlay(deacl_gdf, bbox.to_crs(deacl_gdf.crs)) + deacl_gdf = deacl_gdf.dissolve("year") # values("year", ascending=True) + + # Apply colours + norm = matplotlib.colors.Normalize(vmin=0, vmax=len(deacl_gdf.index)) + cmap = matplotlib.cm.get_cmap("inferno") + rgba = cmap(norm(deacl_gdf.reset_index().index)) + deacl_gdf["color"] = list(rgba) + deacl_gdf["start_time"] = pd.to_datetime(deacl_gdf.index) + pd.DateOffset(months=0) + deacl_gdf = deacl_gdf.sort_index() + + if len(deacl_gdf.index) > 0: + return deacl_gdf + else: + return None + + +class animation_app(HBox): + def __init__(self): + super().__init__() + + ###################### + # INITIAL ATTRIBUTES # + ###################### + + # Basemap + self.basemap_list = [ + ("ESRI World Imagery", basemap_to_tiles(basemaps.Esri.WorldImagery)), + ("Open Street Map", basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)), + ] + self.basemap = self.basemap_list[0][1] + + # Satellite data + end_date = datetime.datetime.today() + start_date = datetime.datetime( + year=end_date.year - 3, month=end_date.month, day=end_date.day + ) + self.start_date = start_date.strftime("%Y-%m-%d") + self.end_date = end_date.strftime("%Y-%m-%d") + self.dealayer_list = [ + ("Landsat", "Landsat"), + ("Sentinel-2", "Sentinel-2"), + ] + self.dealayer = self.dealayer_list[0][1] + + # Styles + self.styles_list = ["True colour", "False colour"] + self.style = self.styles_list[0] + + # Analysis params + self.resolution = 30 + self.vmin = 0.01 + self.vmax = 0.99 + self.power = 1.0 + self.output_list = [("MP4", "mp4"), ("GIF", "gif")] + self.output_format = self.output_list[0][1] + self.rolling_median = False + self.rolling_median_window = 20 + self.unsharp_mask = False + self.unsharp_mask_radius = 20 + self.unsharp_mask_amount = 0.3 + self.max_size = False + self.width = 900 + self.interval = 100 + self.cloud_mask = False + self.max_cloud_cover = 20 + self.resample_list = [ + ("None", False), + ("Monthly", "1M"), + ("Quarterly", "Q-DEC"), + ("Yearly", "1Y"), + ] + self.resample_freq = self.resample_list[0][1] + self.deacoastlines = False + + # Drawing params + self.target = None + self.action = None + self.gdf_drawn = None + + # Data load params + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + ################## + # HEADER FOR APP # + ################## + + # Create the Header widget + header_title_text = ( + "

Digital Earth Africa satellite imagery animations

" + ) + instruction_text = ( + "

Select the desired satellite data, imagery date range " + "and image style, then zoom in and draw a rectangle to " + "select an area export as a satellite imagery time-series " + "animation.

" + ) + self.header = deawidgets.create_html(f"{header_title_text}{instruction_text}") + self.header.layout = make_box_layout() + + ##################################### + # HANDLER FUNCTION FOR DRAW CONTROL # + ##################################### + + # Define the action to take once something is drawn on the map + def update_geojson(target, action, geo_json): + + # Get data from action + self.action = action + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Convert data to geopandas + json_data = json.dumps(geo_json) + binary_data = json_data.encode() + io = BytesIO(binary_data) + io.seek(0) + gdf = gpd.read_file(io) + gdf.crs = "EPSG:4326" + + # Convert to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute area + gdf_drawn_nsidc = gdf.copy().to_crs("EPSG:6933") + m2_per_ha = 10000 + area = gdf_drawn_nsidc.area.values[0] / m2_per_ha + polyarea_label = "Total area of satellite data to extract" + polyarea_text = f"{polyarea_label}: {area:.2f} ha" + + # Test area size + if self.max_size: + confirmation_text = ( + ' ' + "(Overriding maximum size limit; use with caution as may lead to memory issues)" + ) + self.header.value = ( + header_title_text + + instruction_text + + polyarea_text + + confirmation_text + ) + self.gdf_drawn = gdf + elif area <= 50000: + confirmation_text = ( + ' ' + "(Area to extract falls within " + "recommended 50000 ha limit)" + ) + self.header.value = ( + header_title_text + + instruction_text + + polyarea_text + + confirmation_text + ) + self.gdf_drawn = gdf + else: + warning_text = ( + ' ' + "(Area to extract is too large, " + "please select an area less than 50000 " + "ha)" + ) + self.header.value = ( + header_title_text + instruction_text + polyarea_text + warning_text + ) + self.gdf_drawn = None + + ########################### + # WIDGETS FOR APP OUTPUTS # + ########################### + + self.status_info = Output(layout=make_box_layout()) + self.output_plot = Output(layout=make_box_layout()) + + ######################################### + # MAP WIDGET, DRAWING TOOLS, WMS LAYERS # + ######################################### + + # Create drawing tools + desired_drawtools = ["rectangle"] + draw_control = deawidgets.create_drawcontrol(desired_drawtools) + + # Begin by displaying an empty layer group, and update the group with desired WMS on interaction. + self.map_layers = LayerGroup(layers=()) + self.map_layers.name = "Map Overlays" + + # Create map widget + self.m = deawidgets.create_map(map_center=(5.65, 26.17), zoom_level=13) + self.m.layout = make_box_layout() + + # Add tools to map widget + self.m.add_control(draw_control) + self.m.add_layer(self.map_layers) + + # Update all maps to starting defaults + update_map_layers(self) + + ############################ + # WIDGETS FOR APP CONTROLS # + ############################ + + # Create parameter widgets + dropdown_basemap = deawidgets.create_dropdown( + self.basemap_list, self.basemap_list[0][1] + ) + dropdown_dealayer = deawidgets.create_dropdown( + self.dealayer_list, self.dealayer_list[0][1] + ) + dropdown_output = deawidgets.create_dropdown( + self.output_list, self.output_list[0][1] + ) + date_picker_start = deawidgets.create_datepicker( + value=start_date, + ) + date_picker_end = deawidgets.create_datepicker( + value=end_date, + ) + dropdown_styles = deawidgets.create_dropdown( + self.styles_list, self.styles_list[0] + ) + slider_percentile = widgets.FloatRangeSlider( + value=[0.01, 0.99], + min=0, + max=1, + step=0.001, + description="", + layout={"width": "85%"}, + ) + run_button = create_expanded_button("Generate animation", "info") + + floatslider_max_cloud_cover = widgets.IntSlider( + value=20, + min=0, + max=100, + step=1, + description="", + layout={"width": "85%"}, + ) + + checkbox_rolling_median = deawidgets.create_checkbox( + self.rolling_median, + "Apply rolling median
to produce smooth,
cloud-free animations", + layout={"width": "90%", + "height": "4em"}, + ) + text_rolling_median_window = widgets.IntText( + value=20, + step=1, + description="
Rolling window (timesteps)", + layout={ + "width": "85%", + "margin": "0px", + "padding": "0px", + "display": "none", + }, + ) + + # Expandable advanced section + text_interval = widgets.IntText( + value=100, description="", step=50, layout={"width": "95%"} + ) + text_resolution = widgets.FloatText( + value=30, + description="", + layout={"width": "95%", "margin": "0px", "padding": "0px"}, + ) + text_width = widgets.IntText( + value=900, description="", step=50, layout={"width": "95%"} + ) + dropdown_resampling = deawidgets.create_dropdown( + self.resample_list, + self.resample_freq, + description="", + layout={"width": "95%"}, + ) + checkbox_cloud_mask = deawidgets.create_checkbox( + self.cloud_mask, "Mask out cloudy
pixels", layout={"width": "95%", "height": "auto"} + ) + slider_power = widgets.FloatSlider( + value=1.0, + min=0.01, + max=1.0, + step=0.01, + description="", + layout={"width": "95%"}, + ) + checkbox_unsharp_mask = deawidgets.create_checkbox( + self.unsharp_mask, "Enable", layout={"width": "95%"} + ) + text_unsharp_mask_radius = widgets.FloatText( + value=20, + step=1, + description="Radius", + layout={ + "width": "95%", + "margin": "0px", + "padding": "0px", + "display": "none", + }, + ) + text_unsharp_mask_amount = widgets.FloatText( + value=0.3, + step=0.1, + description="Amount", + layout={ + "width": "95%", + "margin": "0px", + "padding": "0px", + "display": "none", + }, + ) + checkbox_deacoastlines = deawidgets.create_checkbox( + self.deacoastlines, "Add DE Africa Coastlines overlay", layout={"width": "95%"} + ) + checkbox_max_size = deawidgets.create_checkbox( + self.max_size, "Enable", layout={"width": "95%"} + ) + expand_box = widgets.VBox( + [ + HTML("Frame interval (milliseconds):"), + text_interval, + HTML("
Resolution (metres):"), + text_resolution, + HTML("
Width of output animation in pixels:"), + text_width, + HTML("
Apply temporal resampling:"), + dropdown_resampling, + HTML("
"), + checkbox_cloud_mask, + checkbox_deacoastlines, + HTML("
Apply power transformation to darken bright features:"), + slider_power, + HTML("
Apply unsharp masking to sharpen imagery:"), + checkbox_unsharp_mask, + text_unsharp_mask_radius, + text_unsharp_mask_amount, + HTML( + "
Override maximum size limit: (use with caution; may cause memory issues/crashes)" + ), + checkbox_max_size, + ], + ) + + expand = widgets.Accordion( + children=[expand_box], + selected_index=None, + ) + expand.set_title(0, "Advanced") + + # Add specific dialogs to class so they can be modified + self.text_resolution = text_resolution + self.text_unsharp_mask_radius = text_unsharp_mask_radius + self.text_unsharp_mask_amount = text_unsharp_mask_amount + self.text_rolling_median_window = text_rolling_median_window + + #################################### + # UPDATE FUNCTIONS FOR EACH WIDGET # + #################################### + + # Run update functions whenever various widgets are changed. + date_picker_start.observe(self.update_start_date, "value") + date_picker_end.observe(self.update_end_date, "value") + dropdown_basemap.observe(self.update_basemap, "value") + dropdown_dealayer.observe(self.update_dealayer, "value") + dropdown_styles.observe(self.update_styles, "value") + + slider_percentile.observe(self.update_slider_percentile, "value") + floatslider_max_cloud_cover.observe( + self.update_floatslider_max_cloud_cover, "value" + ) + checkbox_rolling_median.observe(self.update_checkbox_rolling_median, "value") + text_rolling_median_window.observe( + self.update_text_rolling_median_window, "value" + ) + dropdown_output.observe(self.update_output, "value") + run_button.on_click(self.run_app) + draw_control.on_draw(update_geojson) + + # Advanced params + text_resolution.observe(self.update_text_resolution, "value") + slider_power.observe(self.update_slider_power, "value") + text_width.observe(self.update_width, "value") + text_interval.observe(self.update_interval, "value") + dropdown_resampling.observe(self.update_dropdown_resampling, "value") + checkbox_cloud_mask.observe(self.update_checkbox_cloud_mask, "value") + checkbox_unsharp_mask.observe(self.update_checkbox_unsharp_mask, "value") + text_unsharp_mask_radius.observe(self.update_text_unsharp_mask_radius, "value") + text_unsharp_mask_amount.observe(self.update_text_unsharp_mask_amount, "value") + checkbox_deacoastlines.observe(self.update_deacoastlines, "value") + checkbox_max_size.observe(self.update_checkbox_max_size, "value") + + ################################## + # COLLECTION OF ALL APP CONTROLS # + ################################## + + parameter_selection = VBox( + [ + HTML("Satellite imagery:"), + dropdown_dealayer, + HTML("Start date:"), + date_picker_start, + HTML("End date:"), + date_picker_end, + HTML("Style:"), + dropdown_styles, + HTML("Colour percentile stretch:"), + slider_percentile, + HTML("Maximum cloud cover (%):"), + floatslider_max_cloud_cover, + checkbox_rolling_median, + text_rolling_median_window, + HTML("
Output file format:"), + dropdown_output, + HTML("
"), + expand, + ] + ) + map_selection = VBox( + [ + HTML("
Map overlay:"), + dropdown_basemap, + ] + ) + parameter_selection.layout = make_box_layout() + map_selection.layout = make_box_layout() + + ############################### + # SPECIFICATION OF APP LAYOUT # + ############################### + + # 0 1 2 3 4 5 6 7 8 9 + # --------------------------------------------- + # 0 | Header | Map sel. | + # |-------------------------------------------| + # 1 | Params | | + # 2 | | | + # 3 | | | + # 4 | | Map | + # 5 | | | + # |--------| | + # 6 | Run | | + # |-------------------------------------------| + # 7 | Status info | Figure/output | + # 8 | | | + # 9 | | | + # 10 | | | + # 11 --------------------------------------------- + + # Create the layout #[rowspan, colspan] + grid = GridspecLayout(12, 10, height="1500px", width="auto") + + # Header and controls + grid[0, :8] = self.header + grid[0, 8:] = map_selection + grid[1:6, 0:2] = parameter_selection + grid[6, 0:2] = run_button + + # Status info, map and plot + grid[1:7, 2:] = self.m # map + grid[7:, 0:4] = self.status_info + grid[7:, 4:] = self.output_plot + + # Display using HBox children attribute + self.children = [grid] + + ###################################### + # DEFINITION OF ALL UPDATE FUNCTIONS # + ###################################### + + # Update date + def update_start_date(self, change): + self.start_date = str(change.new) + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Update date + def update_end_date(self, change): + self.end_date = str(change.new) + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Update colour stretch + def update_slider_percentile(self, change): + self.vmin, self.vmax = change.new + + # Update power transform + def update_slider_power(self, change): + self.power = change.new + + # Update good data slider + def update_floatslider_max_cloud_cover(self, change): + self.max_cloud_cover = change.new + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Enable unsharp masking and show/hide custom params + def update_checkbox_unsharp_mask(self, change): + self.unsharp_mask = change.new + + # Show unsharp masking params in menu if activated + if change.new: + self.text_unsharp_mask_radius.layout.display = "block" + self.text_unsharp_mask_amount.layout.display = "block" + else: + self.text_unsharp_mask_radius.layout.display = "none" + self.text_unsharp_mask_amount.layout.display = "none" + + # Change unsharp masking radius + def update_text_unsharp_mask_radius(self, change): + self.unsharp_mask_radius = change.new + + # Change unsharp masking amount + def update_text_unsharp_mask_amount(self, change): + self.unsharp_mask_amount = change.new + + # Enable rolling median and show/hide custom params + def update_checkbox_rolling_median(self, change): + self.rolling_median = change.new + + # Show rolling median params in menu if activated + if change.new: + self.text_rolling_median_window.layout.display = "block" + else: + self.text_rolling_median_window.layout.display = "none" + + # Change rolling median window + def update_text_rolling_median_window(self, change): + self.rolling_median_window = change.new + + # Override max size limit + def update_checkbox_max_size(self, change): + self.max_size = change.new + + # Add DE Africa Coastlines overlay + def update_deacoastlines(self, change): + self.deacoastlines = change.new + + # Apply cloud mask in load_ard + def update_checkbox_cloud_mask(self, change): + self.cloud_mask = change.new + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Override min width + def update_width(self, change): + self.width = change.new + + # Override interval + def update_interval(self, change): + self.interval = change.new + + # Update resolution + def update_text_resolution(self, change): + self.resolution = change.new + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Change layers shown on the map + def update_dealayer(self, change): + self.dealayer = change.new + + if change.new == "Landsat": + self.text_resolution.value = 30 + + else: + self.text_resolution.value = 10 + + # Update basemap + def update_basemap(self, change): + self.basemap = change.new + update_map_layers(self) + + # Set imagery style + def update_styles(self, change): + self.style = change.new + + # Clear data load params to trigger data re-load + self.timeseries_ds = None + self.load_params = None + self.query_params = None + + # Set output file format + def update_output(self, change): + self.output_format = change.new + + # Set output file format + def update_dropdown_resampling(self, change): + self.resample_freq = change.new + + def run_app(self, change): + + # Clear progress bar and output areas before running + self.status_info.clear_output() + self.output_plot.clear_output() + + # Verify that polygon was drawn + if self.gdf_drawn is not None: + + with self.status_info: + + # Load data and add to attribute + if self.timeseries_ds is None: + self.timeseries_ds = extract_data(self) + + else: + print("Using previously loaded data") + + if self.timeseries_ds is not None: + + with self.status_info: + + # Create unique file name + centre_coords = self.gdf_drawn.geometry[0].centroid.coords[0][::-1] + site = reverse_geocode(coords=centre_coords) + fname = ( + f"{self.dealayer}_{site}_{self.start_date}_" + f"{self.end_date}_{self.style}_{self.resolution:.0f}m." + f"{self.output_format}".replace(" ", "") + .replace(",", "") + .lower() + ) + + print( + f"\nExporting animation for {site}.\nThis may take several minutes..." + ) + + ############ + # Plotting # + ############ + + with self.output_plot: + plot_data(self, fname) + + else: + with self.status_info: + print( + "No satellite data found in the selected area. " + "Please select a new rectangle over an area with " + "satellite imagery." + ) + + else: + with self.status_info: + print( + 'Please draw a valid rectangle on the map, then press "Generate animation".' + ) \ No newline at end of file diff --git a/deafrica_tools/app/changefilmstrips.py b/deafrica_tools/app/changefilmstrips.py new file mode 100644 index 0000000..31fb2d8 --- /dev/null +++ b/deafrica_tools/app/changefilmstrips.py @@ -0,0 +1,275 @@ +""" +Loading and interacting with data in the change filmstrips notebook, +inside the Real_world_examples folder. +""" + +# Load modules +import os +import dask +import datacube +import warnings +import numpy as np +import pandas as pd +import xarray as xr +import matplotlib.pyplot as plt +from odc.algo import geomedian_with_mads +from odc.ui import select_on_a_map +from dask.utils import parse_bytes +from datacube.utils.geometry import CRS, assign_crs +from datacube.utils.rio import configure_s3_access +from datacube.utils.dask import start_local_dask +from ipyleaflet import basemaps, basemap_to_tiles + +# Load utility functions +from deafrica_tools.datahandling import load_ard, mostcommon_crs +from deafrica_tools.dask import create_local_dask_cluster + + +def run_filmstrip_app( + output_name, + time_range, + time_step, + tide_range=(0.0, 1.0), + resolution=(-30, 30), + max_cloud=0.5, + ls7_slc_off=False, + size_limit=10000, +): + """ + An interactive app that allows the user to select a region from a + map, then load Digital Earth Africa Landsat data and combine it + using the geometric median ("geomedian") statistic to reveal the + median or 'typical' appearance of the landscape for a series of + time periods. + + The results for each time period are combined into a 'filmstrip' + plot which visualises how the landscape has changed in appearance + across time, with a 'change heatmap' panel highlighting potential + areas of greatest change. + + For coastal applications, the analysis can be customised to select + only satellite images obtained during a specific tidal range + (e.g. low, average or high tide). + + Last modified: April 2020 + + Parameters + ---------- + output_name : str + A name that will be used to name the output filmstrip plot file. + time_range : tuple + A tuple giving the date range to analyse + (e.g. `time_range = ('1988-01-01', '2017-12-31')`). + time_step : dict + This parameter sets the length of the time periods to compare + (e.g. `time_step = {'years': 5}` will generate one filmstrip + plot for every five years of data; `time_step = {'months': 18}` + will generate one plot for each 18 month period etc. Time + periods are counted from the first value given in `time_range`. + tide_range : tuple, optional + An optional parameter that can be used to generate filmstrip + plots based on specific ocean tide conditions. This can be + valuable for analysing change consistently along the coast. + For example, `tide_range = (0.0, 0.2)` will select only + satellite images acquired at the lowest 20% of tides; + `tide_range = (0.8, 1.0)` will select images from the highest + 20% of tides. The default is `tide_range = (0.0, 1.0)` which + will select all images regardless of tide. + resolution : tuple, optional + The spatial resolution to load data. The default is + `resolution = (-30, 30)`, which will load data at 30 m pixel + resolution. Increasing this (e.g. to `resolution = (-100, 100)`) + can be useful for loading large spatial extents. + max_cloud : float, optional + This parameter can be used to exclude satellite images with + excessive cloud. The default is `0.5`, which will keep all images + with less than 50% cloud. + ls7_slc_off : bool, optional + An optional boolean indicating whether to include data from + after the Landsat 7 SLC failure (i.e. SLC-off). Defaults to + False, which removes all Landsat 7 observations > May 31 2003. + size_limit : int, optional + An optional integer (in hectares) specifying the size limit + for the data query. Queries larger than this size will receive + a warning that he data query is too large (and may + therefore result in memory errors). + + + Returns + ------- + ds_geomedian : xarray Dataset + An xarray dataset containing geomedian composites for each + timestep in the analysis. + + """ + + ######################## + # Select and load data # + ######################## + + # Define centre_coords as a global variable + global centre_coords + + # Test if centre_coords is in the global namespace; + # use default value if it isn't + if "centre_coords" not in globals(): + centre_coords = (6.587292, 1.532833) + + # Plot interactive map to select area + basemap = basemap_to_tiles(basemaps.Esri.WorldImagery) + geopolygon = select_on_a_map(height="600px", + layers=(basemap,), + center=centre_coords, + zoom=14) + + # Set centre coords based on most recent selection to re-focus + # subsequent data selections + centre_coords = geopolygon.centroid.points[0][::-1] + + # Test size of selected area + msq_per_hectare = 10000 + area = geopolygon.to_crs(crs=CRS("epsg:6933")).area / msq_per_hectare + radius = np.round(np.sqrt(size_limit), 1) + if area > size_limit: + print(f"Warning: Your selected area is {area:.00f} hectares. " + f"Please select an area of less than {size_limit} hectares." + f"\nTo select a smaller area, re-run the cell " + f"above and draw a new polygon.") + + else: + + print("Starting analysis...") + + # Connect to datacube database + dc = datacube.Datacube(app="Change_filmstrips") + + # Configure local dask cluster + client = create_local_dask_cluster(return_client=True) + + # Obtain native CRS + crs = mostcommon_crs(dc=dc, + product="ls8_sr", + query={ + "time": "2014", + "geopolygon": geopolygon + }) + + # Create query based on time range, area selected, custom params + query = { + "time": time_range, + "geopolygon": geopolygon, + "output_crs": crs, + "resolution": resolution, + "dask_chunks": { + "x": 3000, + "y": 3000 + }, + "align": (resolution[1] / 2.0, resolution[1] / 2.0), + } + + # Load data from all three Landsats + warnings.filterwarnings("ignore") + ds = load_ard( + dc=dc, + measurements=["red", "green", "blue"], + products=["ls5_sr", "ls7_sr", "ls8_sr"], + min_gooddata=max_cloud, + ls7_slc_off=ls7_slc_off, + **query, + ) + + # Optionally calculate tides for each timestep in the satellite + # dataset and drop any observations out side this range + if tide_range != (0.0, 1.0): + from deafrica_tools.coastal import tidal_tag + ds = tidal_tag(ds=ds, tidepost_lat=None, tidepost_lon=None) + min_tide, max_tide = ds.tide_height.quantile(tide_range).values + ds = ds.sel(time=(ds.tide_height >= min_tide) & + (ds.tide_height <= max_tide)) + ds = ds.drop("tide_height") + print(f" Keeping {len(ds.time)} observations with tides " + f"between {min_tide:.2f} and {max_tide:.2f} m") + + # Create time step ranges to generate filmstrips from + bins_dt = pd.date_range(start=time_range[0], + end=time_range[1], + freq=pd.DateOffset(**time_step)) + + # Bin all satellite observations by timestep. If some observations + # fall outside the upper bin, label these with the highest bin + labels = bins_dt.astype("str") + time_steps = (pd.cut(ds.time.values, bins_dt, + labels=labels[:-1]).add_categories( + labels[-1]).fillna(labels[-1])) + + time_steps_var = xr.DataArray(time_steps, [("time", ds.time.values)], + name="timestep") + + # Resample data temporally into time steps, and compute geomedians + ds_geomedian = (ds.groupby(time_steps_var).apply( + lambda ds_subset: geomedian_with_mads( + ds_subset, compute_mads=False, compute_count=False))) + + print("\nGenerating geomedian composites and plotting " + "filmstrips... (click the Dashboard link above for status)") + ds_geomedian = ds_geomedian.compute() + + # Reset CRS that is lost during geomedian compositing + ds_geomedian = assign_crs(ds_geomedian, crs=ds.geobox.crs) + + ############ + # Plotting # + ############ + + # Convert to array and extract vmin/vmax + output_array = ds_geomedian[["red", "green", "blue"]].to_array() + percentiles = output_array.quantile(q=(0.02, 0.98)).values + + # Create the plot with one subplot more than timesteps in the + # dataset. Figure width is set based on the number of subplots + # and aspect ratio + n_obs = output_array.sizes["timestep"] + ratio = output_array.sizes["x"] / output_array.sizes["y"] + fig, axes = plt.subplots(1, + n_obs + 1, + figsize=(5 * ratio * (n_obs + 1), 5)) + fig.subplots_adjust(wspace=0.05, hspace=0.05) + + # Add timesteps to the plot, set aspect to equal to preserve shape + for i, ax_i in enumerate(axes.flatten()[:n_obs]): + output_array.isel(timestep=i).plot.imshow(ax=ax_i, + vmin=percentiles[0], + vmax=percentiles[1]) + ax_i.get_xaxis().set_visible(False) + ax_i.get_yaxis().set_visible(False) + ax_i.set_aspect("equal") + + # Add change heatmap panel to final subplot. Heatmap is computed + # by first taking the log of the array (so change in dark areas + # can be identified), then computing standard deviation between + # all timesteps + (np.log(output_array).std(dim=["timestep"]).mean( + dim="variable").plot.imshow(ax=axes.flatten()[-1], + robust=True, + cmap="magma", + add_colorbar=False)) + axes.flatten()[-1].get_xaxis().set_visible(False) + axes.flatten()[-1].get_yaxis().set_visible(False) + axes.flatten()[-1].set_aspect("equal") + axes.flatten()[-1].set_title("Change heatmap") + + # Export to file + date_string = "_".join(time_range) + ts_v = list(time_step.values())[0] + ts_k = list(time_step.keys())[0] + fig.savefig( + f"filmstrip_{output_name}_{date_string}_{ts_v}{ts_k}.png", + dpi=150, + bbox_inches="tight", + pad_inches=0.1, + ) + + # close dask client + client.shutdown() + + return ds_geomedian diff --git a/deafrica_tools/app/crophealth.py b/deafrica_tools/app/crophealth.py new file mode 100644 index 0000000..f6fd091 --- /dev/null +++ b/deafrica_tools/app/crophealth.py @@ -0,0 +1,337 @@ +# crophealth.py +''' +Functions for loading and interacting with data in the crop health notebook, + inside the Real_world_examples folder. +''' + +# Load modules + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +from ipyleaflet import ( + Map, + GeoJSON, + DrawControl, + basemaps +) +import datetime as dt +import datacube +from osgeo import ogr +import matplotlib as mpl +import matplotlib.pyplot as plt +import rasterio +from rasterio.features import geometry_mask +import xarray as xr +from IPython.display import display +import warnings +import ipywidgets as widgets +import json +import geopandas as gpd +from io import BytesIO + +# Load utility functions +from deafrica_tools.datahandling import load_ard +from deafrica_tools.spatial import xr_rasterize +from deafrica_tools.bandindices import calculate_indices + + +def load_crophealth_data(lat, lon, buffer, date): + """ + Loads Sentinel-2 analysis-ready data (ARD) product for the crop health + case-study area over the last two years. + Last modified: April 2020 + + Parameters + ---------- + lat: float + The central latitude to analyse + lon: float + The central longitude to analyse + buffer: + The number of square degrees to load around the central latitude and longitude. + For reasonable loading times, set this as `0.1` or lower. + date: + The most recent date to show data for. + The app will automatically load all data available for the two years prior to this date. + + Returns + ---------- + ds: xarray.Dataset + data set containing combined, masked data + Masked values are set to 'nan' + """ + + # Suppress warnings + warnings.filterwarnings('ignore') + + # Initialise the data cube. 'app' argument is used to identify this app + dc = datacube.Datacube(app='Crophealth-app') + + # Define area to load + latitude = (lat - buffer, lat + buffer) + longitude = (lon - buffer, lon + buffer) + + # Specify the date range + # Calculated as today's date, subtract 730 days to collect two years of data + # Dates are converted to strings as required by loading function below + end_date = dt.datetime.strptime(date, "%Y-%m-%d") + start_date = end_date - dt.timedelta(days=730) + + time = (start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d")) + + # Construct the data cube query + products = ["s2_l2a"] + + query = { + 'x': longitude, + 'y': latitude, + 'time': time, + 'measurements': [ + 'red', + 'green', + 'blue', + 'nir', + 'swir_2' + ], + 'output_crs': 'EPSG:6933', + 'resolution': (-20, 20) + } + + # Load the data and mask out bad quality pixels + ds = load_ard(dc, products=products, min_gooddata=0.5, **query) + + # Calculate the normalised difference vegetation index (NDVI) across + # all pixels for each image. + # This is stored as an attribute of the data + ds = calculate_indices(ds, index='NDVI', satellite_mission='s2') + + # Return the data + return(ds) + + +def run_crophealth_app(ds, lat, lon, buffer): + """ + Plots an interactive map of the crop health case-study area and allows + the user to draw polygons. This returns a plot of the average NDVI value + in the polygon area. + Last modified: January 2020 + + Parameters + ---------- + ds: xarray.Dataset + data set containing combined, masked data + Masked values are set to 'nan' + lat: float + The central latitude corresponding to the area of loaded ds + lon: float + The central longitude corresponding to the area of loaded ds + buffer: + The number of square degrees to load around the central latitude and longitude. + For reasonable loading times, set this as `0.1` or lower. + """ + + # Suppress warnings + warnings.filterwarnings('ignore') + + # Update plotting functionality through rcParams + mpl.rcParams.update({'figure.autolayout': True}) + + # Define polygon bounds + latitude = (lat - buffer, lat + buffer) + longitude = (lon - buffer, lon + buffer) + + # Define the bounding box that will be overlayed on the interactive map + # The bounds are hard-coded to match those from the loaded data + geom_obj = { + "type": "Feature", + "properties": { + "style": { + "stroke": True, + "color": 'red', + "weight": 4, + "opacity": 0.8, + "fill": True, + "fillColor": False, + "fillOpacity": 0, + "showArea": True, + "clickable": True + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + longitude[0], + latitude[0] + ], + [ + longitude[1], + latitude[0] + ], + [ + longitude[1], + latitude[1] + ], + [ + longitude[0], + latitude[1] + ], + [ + longitude[0], + latitude[0] + ] + ] + ] + } + } + + # Create a map geometry from the geom_obj dictionary + # center specifies where the background map view should focus on + # zoom specifies how zoomed in the background map should be + loadeddata_geometry = ogr.CreateGeometryFromJson(str(geom_obj['geometry'])) + loadeddata_center = [ + loadeddata_geometry.Centroid().GetY(), + loadeddata_geometry.Centroid().GetX() + ] + loadeddata_zoom = 16 + + # define the study area map + studyarea_map = Map( + center=loadeddata_center, + zoom=loadeddata_zoom, + basemap=basemaps.Esri.WorldImagery + ) + + # define the drawing controls + studyarea_drawctrl = DrawControl( + polygon={"shapeOptions": {"fillOpacity": 0}}, + marker={}, + circle={}, + circlemarker={}, + polyline={}, + ) + + # add drawing controls and data bound geometry to the map + studyarea_map.add_control(studyarea_drawctrl) + studyarea_map.add_layer(GeoJSON(data=geom_obj)) + + # Index to count drawn polygons + polygon_number = 0 + + # Define widgets to interact with + instruction = widgets.Output(layout={'border': '1px solid black'}) + with instruction: + print("Draw a polygon within the red box to view a plot of " + "average NDVI over time in that area.") + + info = widgets.Output(layout={'border': '1px solid black'}) + with info: + print("Plot status:") + + fig_display = widgets.Output(layout=widgets.Layout( + width="50%", # proportion of horizontal space taken by plot + )) + + with fig_display: + plt.ioff() + fig, ax = plt.subplots(figsize=(8, 6)) + ax.set_ylim([0, 1]) + + colour_list = plt.rcParams['axes.prop_cycle'].by_key()['color'] + + # Function to execute each time something is drawn on the map + def handle_draw(self, action, geo_json): + nonlocal polygon_number + + # Execute behaviour based on what the user draws + if geo_json['geometry']['type'] == 'Polygon': + + info.clear_output(wait=True) # wait=True reduces flicker effect + + # Save geojson polygon to io temporary file to be rasterized later + jsonData = json.dumps(geo_json) + binaryData = jsonData.encode() + io = BytesIO(binaryData) + io.seek(0) + + # Read the polygon as a geopandas dataframe + gdf = gpd.read_file(io) + gdf.crs = "EPSG:4326" + + # Convert the drawn geometry to pixel coordinates + xr_poly = xr_rasterize(gdf, ds.NDVI.isel(time=0), crs='EPSG:6933') + + # Construct a mask to only select pixels within the drawn polygon + masked_ds = ds.NDVI.where(xr_poly) + + masked_ds_mean = masked_ds.mean(dim=['x', 'y'], skipna=True) + colour = colour_list[polygon_number % len(colour_list)] + + # Add a layer to the map to make the most recently drawn polygon + # the same colour as the line on the plot + studyarea_map.add_layer( + GeoJSON( + data=geo_json, + style={ + 'color': colour, + 'opacity': 1, + 'weight': 4.5, + 'fillOpacity': 0.0 + } + ) + ) + + # add new data to the plot + xr.plot.plot( + masked_ds_mean, + marker='*', + color=colour, + ax=ax + ) + + # reset titles back to custom + ax.set_title("Average NDVI from Sentinel-2") + ax.set_xlabel("Date") + ax.set_ylabel("NDVI") + + # refresh display + fig_display.clear_output(wait=True) # wait=True reduces flicker effect + with fig_display: + display(fig) + + with info: + print("Plot status: polygon sucessfully added to plot.") + + # Iterate the polygon number before drawing another polygon + polygon_number = polygon_number + 1 + + else: + info.clear_output(wait=True) + with info: + print("Plot status: this drawing tool is not currently " + "supported. Please use the polygon tool.") + + # call to say activate handle_draw function on draw + studyarea_drawctrl.on_draw(handle_draw) + + with fig_display: + # TODO: update with user friendly something + display(widgets.HTML("")) + + # Construct UI: + # +-----------------------+ + # | instruction | + # +-----------+-----------+ + # | map | plot | + # | | | + # +-----------+-----------+ + # | info | + # +-----------------------+ + ui = widgets.VBox([instruction, + widgets.HBox([studyarea_map, fig_display]), + info]) + display(ui) diff --git a/deafrica_tools/app/deacoastlines.py b/deafrica_tools/app/deacoastlines.py new file mode 100644 index 0000000..2b00a48 --- /dev/null +++ b/deafrica_tools/app/deacoastlines.py @@ -0,0 +1,505 @@ +""" +Digital Earth Africa Coastline widget, which can be used to +interactively extract shoreline data using transects. +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import fiona +import sys +import datacube +import warnings +import matplotlib.pyplot as plt +from datacube.utils.geometry import CRS +from ipyleaflet import ( + WMSLayer, + basemaps, + basemap_to_tiles, + Map, + DrawControl, + WidgetControl, + LayerGroup, + LayersControl, + GeoData, +) +from traitlets import Unicode +from ipywidgets import ( + GridspecLayout, + Button, + Layout, + HBox, + VBox, + HTML, + Output, +) +import json +import geopandas as gpd +from io import BytesIO +import ipywidgets as widgets + +import deafrica_tools.app.widgetconstructors as deawidgets +from deafrica_tools.coastal import get_coastlines, transect_distances +from owslib.wms import WebMapService + +def make_box_layout(): + return Layout( + # border='solid 1px black', + margin='0px 10px 10px 0px', + padding='5px 5px 5px 5px', + width='100%', + height='100%', + ) + + +def create_expanded_button(description, button_style): + return Button( + description=description, + button_style=button_style, + layout=Layout(width="auto", height="auto"), + ) + + +class transect_app(HBox): + + def __init__(self): + super().__init__() + + ###################### + # INITIAL ATTRIBUTES # + ###################### + + self.output_name = "example_output" + self.export_csv = False + self.export_plot = False + self.product_list = [ + ("ESRI World Imagery", "none"), + ("Open Street Map", "open_street_map"), + ] + self.product = self.product_list[0][1] + self.mode_list = [('Distance', 'distance'), ('Width', 'width')] + self.mode = self.mode_list[0][1] + self.target = None + self.action = None + self.gdf_drawn = None + self.gdf_uploaded = None + + ################## + # HEADER FOR APP # + ################## + + # Create the Header widget + header_title_text = "

Digital Earth Africa Coastlines shoreline transect extraction

" + instruction_text = "Select parameters and draw a transect on the map to extract shoreline data. In distance mode, draw a transect line starting from land that crosses multiple shorelines.
In width mode, draw a transect line that intersects shorelines at least twice. Alternatively, upload an vector file to extract shoreline data for multiple existing transects." + self.header = deawidgets.create_html( + f"{header_title_text}

{instruction_text}

") + self.header.layout = make_box_layout() + + ##################################### + # HANDLER FUNCTION FOR DRAW CONTROL # + ##################################### + + # Define the action to take once something is drawn on the map + def update_geojson(target, action, geo_json): + + # Remove previously uploaded data if present + self.gdf_uploaded = None + fileupload_transects._counter = 0 + + # Get data from action + self.action = action + + # Convert data to geopandas + json_data = json.dumps(geo_json) + binary_data = json_data.encode() + io = BytesIO(binary_data) + io.seek(0) + gdf = gpd.read_file(io) + gdf.crs = "EPSG:4326" + + # Convert to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute area + gdf_drawn_nsidc = gdf.copy().to_crs("EPSG:6933") + m2_per_km2 = 10**6 + area = gdf_drawn_nsidc.envelope.area.values[0] / m2_per_km2 + polyarea_label = 'Total area of DE Africa Coastlines data to extract' + polyarea_text = f"{polyarea_label}: {area:.2f} km2" + + # Test area size + if area <= 50000: + confirmation_text = ' (Area to extract falls within recommended limit; click "Extract shoreline data" to continue)' + self.header.value = header_title_text + polyarea_text + confirmation_text + self.gdf_drawn = gdf + else: + warning_text = ' (Area to extract is too large, please select a smaller transect)' + self.header.value = header_title_text + polyarea_text + warning_text + self.gdf_drawn = None + + ########################### + # WIDGETS FOR APP OUTPUTS # + ########################### + + self.status_info = Output(layout=make_box_layout()) + self.output_plot = Output(layout=make_box_layout()) + + ######################################### + # MAP WIDGET, DRAWING TOOLS, WMS LAYERS # + ######################################### + + # Create drawing tools + desired_drawtools = ['polyline'] + draw_control = deawidgets.create_drawcontrol(desired_drawtools) + + # Load DEACoastLines WMS + deacl_url = "https://geoserver.digitalearth.africa/geoserver/wms" + deacl_layer = "coastlines:DEAfrica_Coastlines" + deacoastlines = WMSLayer( + url=deacl_url, + layers=deacl_layer, + format='image/png', + transparent=True, + attribution='DE Africa Coastlines © 2022 Digital Earth Africa') + + # Begin by displaying an empty layer group, and update the group with desired WMS on interaction. + self.map_layers = LayerGroup(layers=(deacoastlines,)) + self.map_layers.name = 'Map Overlays' + + # Create map widget + self.m = deawidgets.create_map(map_center=(0.5273, 25.1367), + zoom_level=3, + basemap=basemaps.Esri.WorldImagery) + self.m.layout = make_box_layout() + + # Add tools to map widget + self.m.add_control(draw_control) + self.m.add_layer(self.map_layers) + + # Store current basemap for future use + self.basemap = self.m.basemap + + ############################ + # WIDGETS FOR APP CONTROLS # + ############################ + + # Create parameter widgets + text_output_name = deawidgets.create_inputtext(self.output_name, + self.output_name) + checkbox_csv = deawidgets.create_checkbox(self.export_csv, + 'Distance table (.csv)') + checkbox_plot = deawidgets.create_checkbox(self.export_plot, + 'Figure (.png)') + deaoverlay_dropdown = deawidgets.create_dropdown( + self.product_list, self.product_list[0][1]) + mode_dropdown = deawidgets.create_dropdown(self.mode_list, + self.mode_list[0][1]) + run_button = create_expanded_button("Extract shoreline data", "info") + fileupload_transects = widgets.FileUpload(accept='', multiple=True) + + #################################### + # UPDATE FUNCTIONS FOR EACH WIDGET # + #################################### + + # Run update functions whenever various widgets are changed. + text_output_name.observe(self.update_text_output_name, "value") + checkbox_csv.observe(self.update_checkbox_csv, "value") + checkbox_plot.observe(self.update_checkbox_plot, "value") + deaoverlay_dropdown.observe(self.update_deaoverlay, "value") + mode_dropdown.observe(self.update_mode, "value") + run_button.on_click(self.run_app) + draw_control.on_draw(update_geojson) + fileupload_transects.observe(self.update_fileupload_transects, "value") + + ################################## + # COLLECTION OF ALL APP CONTROLS # + ################################## + + parameter_selection = VBox([ + HTML("Output name:"), text_output_name, + HTML( + 'Transect extraction mode:
' + ), + mode_dropdown, + HTML("
Output files:
"), + checkbox_plot, + checkbox_csv, + HTML( + "
Advanced
Upload a GeoJSON or ESRI " + "Shapefile (<5 mb) containing one or more transect lines.
"), + fileupload_transects + ]) + map_selection = VBox([ + HTML("
Map overlay:"), + deaoverlay_dropdown, + ]) + parameter_selection.layout = make_box_layout() + map_selection.layout = make_box_layout() + + ############################### + # SPECIFICATION OF APP LAYOUT # + ############################### + + # 0 1 2 3 4 5 6 7 8 9 + # --------------------------------------------- + # 0 | Header | Map sel. | + # --------------------------------------------- + # 1 | Params | | + # 2 | | | + # 3 | | | + # 4 | | Map | + # 5 | | | + # ---------- | + # 6 | Run | | + # --------------------------------------------- + # 7 | Status info | + # --------------------------------------------- + # 8 | | + # 9 | Output/figure | + # 10 | | + # 11 | ------------------------------------------| + + # Create the layout #[rowspan, colspan] + grid = GridspecLayout(12, 10, height="1350px", width="auto") + + # Header and controls + grid[0, :8] = self.header + grid[0, 8:] = map_selection + grid[1:6, 0:2] = parameter_selection + grid[6, 0:2] = run_button + + # Status info, map and plot + grid[1:7, 2:] = self.m # map + grid[7:8, :] = self.status_info + grid[8:, :] = self.output_plot + + # Display using HBox children attribute + self.children = [grid] + + ###################################### + # DEFINITION OF ALL UPDATE FUNCTIONS # + ###################################### + + # Set the output csv + def update_fileupload_transects(self, change): + + # Clear any drawn data if present + self.gdf_drawn = None + + # Save to file + for uploaded_filename in change.new.keys(): + with open(uploaded_filename, "wb") as output_file: + content = change.new[uploaded_filename]['content'] + output_file.write(content) + + with self.status_info: + + try: + + print('Loading vector data...', end='\r') + valid_files = [ + file for file in change.new.keys() + if file.lower().endswith(('.shp', '.geojson')) + ] + valid_file = valid_files[0] + transect_gdf = (gpd.read_file(valid_file).to_crs( + "EPSG:4326").explode().reset_index(drop=True)) + + # Use ID column if it exists + if 'id' in transect_gdf: + transect_gdf = transect_gdf.set_index('id') + print(f"Uploaded '{valid_file}'; automatically labelling " + "transects using column 'id'.") + else: + print( + f"Uploaded '{valid_file}'; no 'id' column detected, " + f"labelling transects from 0 to {len(transect_gdf.index) - 1}." + ) + + # Create a geodata + geodata = GeoData(geo_dataframe=transect_gdf, + style={ + 'color': 'black', + 'weight': 3 + }) + + # Add to map + xmin, ymin, xmax, ymax = transect_gdf.total_bounds + self.m.fit_bounds([[ymin, xmin], [ymax, xmax]]) + self.m.add_layer(geodata) + + # If completed, add to attribute + self.gdf_uploaded = transect_gdf + + except IndexError: + print( + "Cannot read uploaded files. Please ensure that data is " + "in either GeoJSON or ESRI Shapefile format.", + end='\r') + self.gdf_uploaded = None + + except fiona.errors.DriverError: + print( + "Shapefile is invalid. Please ensure that all shapefile " + "components (e.g. .shp, .shx, .dbf, .prj) are uploaded.", + end='\r') + self.gdf_uploaded = None + + # Set output name + def update_text_output_name(self, change): + self.output_name = change.new + + # Output CSV + def update_checkbox_csv(self, change): + self.export_csv = change.new + + # Output plot + def update_checkbox_plot(self, change): + self.export_plot = change.new + + # Set mode + def update_mode(self, change): + self.mode = change.new + + # Update product + def update_deaoverlay(self, change): + + self.product = change.new + + # Load DE Africa CoastLines WMS + deacl_url = "https://geoserver.digitalearth.africa/geoserver/wms" + deacl_layer = "coastlines:DEAfrica_Coastlines" + deacoastlines = WMSLayer( + url=deacl_url, + layers=deacl_layer, + format="image/png", + transparent=True, + attribution="DE Africa Coastlines © 2022 Digital Earth Africa") + + if self.product == "none": + self.map_layers.clear_layers() + self.map_layers.add_layer(deacoastlines) + + elif self.product == "open_street_map": + self.map_layers.clear_layers() + layer = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik) + self.map_layers.add_layer(layer) + self.map_layers.add_layer(deacoastlines) + + def run_app(self, change): + + # Clear progress bar and output areas before running + self.status_info.clear_output() + self.output_plot.clear_output() + + # Run DE Africa Coastlines analysis + with self.status_info: + warnings.filterwarnings("ignore") + + # Load transects from either map or uploaded files + if self.gdf_uploaded is not None: + transect_gdf = self.gdf_uploaded + run_text = 'uploaded file' + elif self.gdf_drawn is not None: + transect_gdf = self.gdf_drawn + transect_gdf.index = [self.output_name] + run_text = 'selected transect' + else: + print(f'No transect drawn or uploaded. Please select a transect on the map, or upload a GeoJSON or ESRI Shapefile.', + end='\r') + transect_gdf = None + + # If valid data was returned, load DEA Coastlines data + if transect_gdf is not None: + + # Load Coastlines data from WFS + deacl_gdf = get_coastlines(bbox=transect_gdf) + + # Test that data was correctly returned + if len(deacl_gdf.index) > 0: + + # Dissolve by year to remove duplicates, then sort by date + deacl_gdf = deacl_gdf.dissolve(by='year', as_index=False) + deacl_gdf['year'] = deacl_gdf.year.astype(int) + deacl_gdf = deacl_gdf.sort_values('year') + deacl_gdf = deacl_gdf.set_index('year') + + else: + print( + "No annual shoreline data was found near the " + "supplied transect. Please draw or select a new " + "transect.", + end='\r') + deacl_gdf = None + + # If valid DEA Coastlines data returned, calculate distances + if deacl_gdf is not None: + print(f'Analysing transect distances using "{self.mode}" mode...', + end='\r') + dist_df = transect_distances( + transect_gdf.to_crs("EPSG:6933"), + deacl_gdf.to_crs("EPSG:6933"), + mode=self.mode) + + # If valid data was produced: + if dist_df.any(axis=None): + + # Successful output + print(f'DE Africa Coastlines data successfully extracted for {run_text}.') + + # Export distance data + if self.export_csv: + + # Create folder if required and set path + out_dir = 'deacoastlines_outputs' + os.makedirs(out_dir, exist_ok=True) + csv_filename = f"{out_dir}/{self.output_name}.csv" + + # Export to file + dist_df.to_csv(csv_filename, index_label="Transect") + print(f'Distance data exported to "{csv_filename}".') + + # Generate plot + with self.output_plot: + + fig, ax = plt.subplots(constrained_layout=True, + figsize=(15, 5.5)) + dist_df.T.plot(ax=ax, linewidth=3) + + ax.legend(frameon=False, ncol=3, title='Transect') + ax.set_title(f"Digital Earth Africa Coastlines transect extraction - {self.output_name}") + ax.set_ylabel(f"Along-transect {self.mode} (m)") + ax.set_xlim(dist_df.T.index[0], dist_df.T.index[-1]) + + # Hide the right and top spines + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + + # Only show ticks on the left and bottom spines + ax.yaxis.set_ticks_position('left') + ax.xaxis.set_ticks_position('bottom') + plt.show() + + # Export plot + with self.status_info: + if self.export_plot: + + # Create folder if required and set path + out_dir = 'deacoastlines_outputs' + os.makedirs(out_dir, exist_ok=True) + figure_filename = f"{out_dir}/{self.output_name}.png" + + # Export to file + fig.savefig(figure_filename) + print(f'Figure exported to "{figure_filename}".') + + else: + print( + "No valid shoreline data intersects with the " + "supplied transect. This can occur if:\n\n" + " - the transect does not intersect with any shorelines\n" + " - the transect intersects with shorelines more than once in 'distance' mode\n" + " - the transect intersects with shorelines only once in 'width' mode\n\n" + "Please draw or upload a new transect.", + end='\r') \ No newline at end of file diff --git a/deafrica_tools/app/forestmonitoring.py b/deafrica_tools/app/forestmonitoring.py new file mode 100644 index 0000000..4e99243 --- /dev/null +++ b/deafrica_tools/app/forestmonitoring.py @@ -0,0 +1,1026 @@ +''' +Functions for loading and interacting with Global Forest Change data in the forest monitoring notebook, inside the Real_world_examples folder. +''' + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import json +import warnings +from io import BytesIO + +import deafrica_tools.app.widgetconstructors as deawidgets +import geopandas as gpd +import ipywidgets as widgets +import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import rioxarray +import xarray as xr +from deafrica_tools.dask import create_local_dask_cluster +from deafrica_tools.spatial import xr_rasterize +from ipyleaflet import ( + DrawControl, + GeoData, + LayerGroup, + LayersControl, + Map, + WidgetControl, + WMSLayer, + basemap_to_tiles, + basemaps, +) +from ipywidgets import HTML, Button, GridspecLayout, HBox, Layout, Output, VBox +from matplotlib.patches import Patch +from traitlets import Unicode + +# Turn off all warnings. +warnings.filterwarnings("ignore") +warnings.simplefilter("ignore") + +def make_box_layout(): + """ + Defines a number of CSS properties that impact how a widget is laid out. + """ + return Layout( # border='solid 1px black', + margin="0px 10px 10px 0px", + padding="5px 5px 5px 5px", + width="100%", + height="100%", + ) + +def create_expanded_button(description, button_style): + """ + Defines a number of CSS properties to create a button to handle mouse clicks. + """ + return Button( + description=description, + button_style=button_style, + layout=Layout(width="auto", height="auto"), + ) + +def load_gfclayer(gdf_drawn, gfclayer): + """ + Loads the selected Global Forest Change layer for the + area drawn on the map widget. + """ + # Configure local dask cluster. + client = create_local_dask_cluster(return_client=True, display_client=True) + + # Get the coordinates of the top-left corner for each Global Forest Change tile, + # covering the area of interest. + min_lat, max_lat = ( + gdf_drawn.bounds.miny.item(), + gdf_drawn.bounds.maxy.item(), + ) + min_lon, max_lon = ( + gdf_drawn.bounds.minx.item(), + gdf_drawn.bounds.maxx.item(), + ) + + lats = np.arange( + np.floor(min_lat / 10) * 10, np.ceil(max_lat / 10) * 10, 10 + ).astype(int) + lons = np.arange( + np.floor(min_lon / 10) * 10, np.ceil(max_lon / 10) * 10, 10 + ).astype(int) + + coord_list = [] + for lat in lats: + lat = lat + 10 + if lat >= 0: + lat_str = f"{lat:02d}N" + else: + lat_str = f"{abs(lat):02d}S" + for lon in lons: + if lon >= 0: + lon_str = f"{lon:03d}E" + else: + lon_str = f"{abs(lon):03d}W" + coord_str = f"{lat_str}_{lon_str}" + coord_list.append(coord_str) + + # Load each Global Forest Change tile covering the area of interest. + base_url = f"https://storage.googleapis.com/earthenginepartners-hansen/GFC-2021-v1.9/Hansen_GFC-2021-v1.9_{gfclayer}_" + dask_chunks = dict(x=2048, y=2048) + + tile_list = [] + for coord in coord_list: + tile_url = f"{base_url}{coord}.tif" + # Load the tile as an xarray.DataArray. + tile = rioxarray.open_rasterio(tile_url, chunks=dask_chunks).squeeze() + tile_list.append(tile) + + # Merge the tiles into a single xarray.DataArray. + ds = xr.combine_by_coords(tile_list) + # Clip the dataset using the bounds of the area of interest. + ds = ds.rio.clip_box( + minx=min_lon - 0.00025, + miny=min_lat - 0.00025, + maxx=max_lon + 0.00025, + maxy=max_lat + 0.00025, + ) + # Rename the y and x variables for DEA convention on xarray.DataArrays where crs="EPSG:4326". + ds = ds.rename({"y": "latitude", "x": "longitude"}) + + # Mask pixels representing no loss (encoded as 0) in the "lossyear" layer. + if gfclayer == "lossyear": + ds = ds.where(ds != 0) + # Mask pixels representing no gain (encoded as 0) in the "gain" layer. + elif gfclayer == "gain": + ds = ds.where(ds != 0) + # Mask pixels with 0 percentage tree canopy cover. + elif gfclayer == "treecover2000": + ds = ds.where(ds != 0) + + # Create a mask from the area of interest GeoDataFrame. + mask = xr_rasterize(gdf_drawn, ds) + # Mask the dataset. + ds = ds.where(mask) + # Convert the xarray.DataArray to a dataset. + ds = ds.to_dataset(name=gfclayer) + # Compute. + ds = ds.compute() + # Assign the "EPSG:4326" CRS to the dataset. + ds.rio.write_crs(4326, inplace=True) + ds = ds.transpose("latitude", "longitude") + + # Close down the dask client. + client.close() + return ds + +def load_all_gfclayers(gdf_drawn): + gfclayers = ["treecover2000", "gain", "lossyear"] + + dataset_list = [] + for layer in gfclayers: + ds = load_gfclayer(gdf_drawn, gfclayer=layer) + dataset_list.append(ds) + + dataset = xr.merge(dataset_list) + return dataset + +def get_gfclayer_treecover2000(gfclayer_ds, gfclayer="treecover2000"): + """ + Preprocess the Global Forest change "treecover2020" layer. + """ + ds = gfclayer_ds[gfclayer] + + # Check if the dataarray is empty. + condition = ds.isnull().all().item() + + if condition: + return None + else: + # Mask the dataset. + mask = np.isnan(ds) + ds_masked = ds.where(mask, 1) + + # Get the pixel count for each unique pixel value in the layer. + counts = np.unique(ds_masked, return_counts=True) + # Remove the counts for pixels with the value np.nan. + index = np.argwhere(np.isnan(counts[0])) + counts_dict = dict( + zip(np.delete(counts[0], index), np.delete(counts[1], index)) + ) + + # Reproject the dataset to EPSG:6933 which uses metres + ds_reprojected = ds_masked.rio.reproject("EPSG:6933") + # Get the area per pixel. + pixel_length = ds_reprojected.geobox.resolution[1] + m_per_km = 1000 + per_pixel_area = (pixel_length / m_per_km) ** 2 + + # Save the results as a pandas DataFrame. + df = pd.DataFrame( + data={ + "Year": ["2000"], + "Tree Cover in km$^2$": np.fromiter(counts_dict.values(), dtype=float) + * per_pixel_area, + } + ) + + # Get the total area. + print_statement = f'Total Forest Cover in {df["Year"].item()}: {round(df["Tree Cover in km$^2$"].item(), 4)} km2' + + # File name to use when exporting results. + file_name = f"forest_cover_in_2000" + + return ds, df, print_statement, file_name + +def get_gfclayer_gain(gfclayer_ds, gfclayer="gain"): + """ + Preprocess the Global Forest Change "gain" layer. + """ + ds = gfclayer_ds[gfclayer] + + # Check if the dataarray is empty. + condition = ds.isnull().all().item() + + if condition: + return None + else: + # Get the pixel count for each unique pixel value in the layer. + counts = np.unique(ds, return_counts=True) + # Remove the counts for pixels with the value np.nan. + index = np.argwhere(np.isnan(counts[0])) + counts_dict = dict( + zip(np.delete(counts[0], index), np.delete(counts[1], index)) + ) + + # Reproject the dataset to EPSG:6933 which uses metres. + ds_reprojected = ds.rio.reproject("EPSG:6933") + # Get the area per pixel. + pixel_length = ds_reprojected.geobox.resolution[1] + m_per_km = 1000 + per_pixel_area = (pixel_length / m_per_km) ** 2 + + # Save the results as a pandas DataFrame. + df = pd.DataFrame( + data={ + "Year": ["2000-2012"], + "Forest Cover Gain in km$^2$": np.fromiter( + counts_dict.values(), dtype=float + ) + * per_pixel_area, + } + ) + + # Get the total area. + print_statement = f'Total Forest Cover Gain {df["Year"].item()}: {round(df["Forest Cover Gain in km$^2$"].item(), 4)} km2' + + # File name to use when exporting results. + file_name = f"forest_cover_gain_from_2000_to_2012" + + return ds, df, print_statement, file_name + +def get_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer="lossyear"): + """ + Preprocess the Global Forest Change "lossyear" layer. + """ + + ds = gfclayer_ds[gfclayer] + + # Mask the dataset to the selected time range. + selected_years = list(range(start_year, end_year + 1)) + mask = ds.isin(selected_years) + ds = ds.where(mask) + + # Check if the dataarray is empty. + condition = ds.isnull().all().item() + + if condition: + return None + else: + # Get the pixel count for each unique pixel value in the layer. + counts = np.unique(ds, return_counts=True) + # Remove the counts for pixels with the value np.nan. + index = np.argwhere(np.isnan(counts[0])) + counts_dict = dict( + zip(np.delete(counts[0], index), np.delete(counts[1], index)) + ) + + # Reproject the dataset to EPSG:6933 which uses metres + ds_reprojected = ds.rio.reproject("EPSG:6933") + # Get the area per pixel. + pixel_length = ds_reprojected.geobox.resolution[1] + m_per_km = 1000 + per_pixel_area = (pixel_length / m_per_km) ** 2 + + # For each year get the area of loss. + # Save the results as a pandas DataFrame. + df = pd.DataFrame( + { + "Year": 2000 + np.fromiter(counts_dict.keys(), dtype=int), + "Forest Cover Loss in km$^2$": np.fromiter( + counts_dict.values(), dtype=float + ) + * per_pixel_area, + } + ) + + # Get the total area. + print_statement = f'Total Forest Cover Loss from {start_year + 2000} to {end_year + 2000}: {round(df["Forest Cover Loss in km$^2$"].sum(), 4)} km2' + + # File name to use when exporting results. + file_name = f"forest_cover_loss_from_{start_year + 2000}_to_{end_year + 2000}" + + return ds, df, print_statement, file_name + +def plot_gfclayer_treecover2000(gfclayer_ds, gfclayer="treecover2000"): + """ + Plot the Global Forest Change "treecover2000" layer. + """ + + if get_gfclayer_treecover2000(gfclayer_ds) is None: + print( + f"No Global Forest Change {gfclayer} layer data found in the selected area. Please select a new polygon over an area with data." + ) + else: + ds, df, print_statement, file_name = get_gfclayer_treecover2000(gfclayer_ds) + + # Export the dataframe as a csv. + df.to_csv(f"{file_name}.csv", index=False) + print(f'Table exported to "{file_name}.csv"') + + # Define the plotting parameters. + figure_width = 10 + figure_length = 10 + title = f"Tree Canopy Cover for the Year 2000" + + # Plot the dataset. + fig, ax = plt.subplots(figsize=(figure_width, figure_length)) + im = ds.plot(cmap="Greens", add_colorbar=False, ax=ax) + # Add a colorbar to the plot. + cbar = plt.colorbar(mappable=im) + cbar.set_label( + "Percentage tree canopy cover for year 2000", labelpad=-65, y=0.25 + ) + # Add a title to the plot. + plt.title(title) + # Save the plot. + plt.savefig(f"{file_name}.png") + print(f'Figure exported to "{file_name}.png"') + plt.show() + + print(print_statement) + +def plot_gfclayer_gain(gfclayer_ds, gfclayer="gain"): + """ + Plot the Global Forest Change "gain" layer. + """ + + if get_gfclayer_gain(gfclayer_ds) is None: + print( + f"No Global Forest Change {gfclayer} layer data found in the selected area. Please select a new polygon over an area with data." + ) + else: + ds, df, print_statement, file_name = get_gfclayer_gain(gfclayer_ds) + + # Export the dataframe as a csv. + df.to_csv(f"{file_name}.csv", index=False) + print(f'Table exported to "{file_name}.csv"') + + # Define the plotting parameters. + color = "#6CAE75" + figure_width = 10 + figure_length = 10 + title = f"Forest Cover Gain from 2000 to 2012" + + # Plot the dataset. + fig, ax = plt.subplots(figsize=(figure_width, figure_length)) + im = ds.plot(cmap=mcolors.ListedColormap([color]), add_colorbar=False, ax=ax) + # Add a legend to the plot. + im.axes.legend( + [Patch(facecolor=color)], + ["Global forest cover gain 2000–2012"], + loc="lower left", + bbox_to_anchor=(1.0, 0.5), + frameon=False, + ) + # Add a title to the plot. + plt.title(title) + # Save the plot. + plt.savefig(f"{file_name}.png") + print(f'Figure exported to "{file_name}.png"') + plt.show() + + print(print_statement) + +def plot_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer="lossyear"): + """ + Plot the Global Forest change "lossyear" layer. + """ + + if ( + get_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer="lossyear") + is None + ): + print( + f"No Global Forest Change {gfclayer} layer data found in the selected area. Please select a new polygon over an area with data." + ) + else: + ds, df, print_statement, file_name = get_gfclayer_lossyear( + gfclayer_ds, start_year, end_year, gfclayer="lossyear" + ) + + # Export the dataframe as a csv. + df.to_csv(f"{file_name}.csv", index=False) + print(f'Table exported to "{file_name}.csv"') + + # Define the plotting parameters. + figure_width = 10 + figure_length = 15 + nrows = 2 + ncols = 1 + title = f"Forest Cover Loss from {start_year + 2000} to {end_year + 2000}" + + # Location of transition from one color to the next on the colormap. + color_levels = list(np.arange(1 - 0.5, 22, 1)) + # Ticks to be displayed. + ticks = list(np.arange(1, 22)) + tick_labels = list(2000 + np.arange(1, 22)) + + # Define the color map to use when plotting. + color_list = [ + "#e6194b", + "#3cb44b", + "#ffe119", + "#4363d8", + "#f58231", + "#911eb4", + "#46f0f0", + "#f032e6", + "#bcf60c", + "#fabebe", + "#008080", + "#e6beff", + "#9a6324", + "#fffac8", + "#800000", + "#aaffc3", + "#808000", + "#ffd8b1", + "#000075", + "#808080", + "#7A306C", + ] + cmap = mcolors.ListedColormap(colors=color_list, N=21) + norm = mcolors.BoundaryNorm(boundaries=color_levels, ncolors=cmap.N) + + # Plot the dataset. + fig, (ax1, ax2) = plt.subplots( + nrows, ncols, figsize=(figure_width, figure_length) + ) + im = ds.plot(ax=ax1, cmap=cmap, norm=norm, add_colorbar=False) + # Add a title to the subplot. + ax1.set_title(title) + # Add a colorbar to the subplot. + cbar = plt.colorbar(mappable=im, ticks=ticks) + cbar.set_label("Year of gross forest cover loss event", labelpad=-60, y=0.25) + cbar.set_ticklabels(tick_labels) + # Plot the second subplot. + df.plot( + x="Year", + y="Forest Cover Loss in km$^2$", + ylabel="Forest Cover Loss in km$^2$", + title=title, + ax=ax2, + ) + # Save the plot. + plt.savefig(f"{file_name}.png") + print(f'Figure exported to "{file_name}.png"') + plt.show() + + print(print_statement) + +def plot_gfclayer_all(gfclayer_ds, start_year, end_year): + """ + Plot all the Global Forest Change Layers loaded. + """ + + # Define the plotting parameters. + figure_width = 10 + figure_length = 10 + treecover_color = "Greens" + gain_color = "yellow" + lossyear_color = "red" + + print_statement_list = [] + filename_list = ["\nTables exported as: "] + + figure_fn = "global_forest_change_all_layers.png" + + # Define the figure. + fig, ax = plt.subplots(figsize=(figure_width, figure_length)) + if ( + get_gfclayer_treecover2000( + gfclayer_ds[["treecover2000"]], gfclayer="treecover2000" + ) + is None + ): + print( + f"No Global Forest Change 'treecover2000' layer data found in the selected area. Please select a new polygon over an area with data." + ) + else: + ( + ds_treecover2000, + df_treecover2000, + print_statement_treecover2000, + file_name_treecover2000, + ) = get_gfclayer_treecover2000( + gfclayer_ds[["treecover2000"]], gfclayer="treecover2000" + ) + # Plot the treecover2000 layer as the background layer. + background = ds_treecover2000.plot( + cmap=treecover_color, add_colorbar=False, ax=ax + ) + # Add a colorbar to the treecover2000 plot. + cbar = plt.colorbar(mappable=background) + cbar.set_label( + "Percentage tree canopy cover for year 2000", labelpad=-65, y=0.25 + ) + # Export the dataframe as a csv. + df_treecover2000.to_csv(f"{file_name_treecover2000}.csv", index=False) + # Add the print statement to the list. + print_statement_list.append(print_statement_treecover2000) + # Add the file name to the list. + filename_list.append(f'"{file_name_treecover2000}.csv"') + + if get_gfclayer_gain(gfclayer_ds[["gain"]], gfclayer="gain") is None: + print( + f"No Global Forest Change 'gain' layer data found in the selected area. Please select a new polygon over an area with data." + ) + else: + ds_gain, df_gain, print_statement_gain, file_name_gain = get_gfclayer_gain( + gfclayer_ds[["gain"]], gfclayer="gain" + ) + # Plot the gain layer. + ds_gain.plot( + ax=ax, cmap=mcolors.ListedColormap([gain_color]), add_colorbar=False + ) + # Export the dataframe as a csv. + df_gain.to_csv(f"{file_name_gain}.csv", index=False) + # Add the print statement to the list. + print_statement_list.append(print_statement_gain) + # Add the file name to the list. + filename_list.append(f'"{file_name_gain}.csv"') + + if ( + get_gfclayer_lossyear( + gfclayer_ds[["lossyear"]], start_year, end_year, gfclayer="lossyear" + ) + is None + ): + print( + f"No Global Forest Change 'lossyear' layer data found in the selected area. Please select a new polygon over an area with data." + ) + else: + ( + ds_lossyear, + df_lossyear, + print_statement_lossyear, + file_name_lossyear, + ) = get_gfclayer_lossyear( + gfclayer_ds[["lossyear"]], start_year, end_year, gfclayer="lossyear" + ) + # Plot the lossyear layer. + ds_lossyear.plot( + ax=ax, cmap=mcolors.ListedColormap([lossyear_color]), add_colorbar=False + ) + # Export the dataframe as a csv. + df_lossyear.to_csv(f"{file_name_lossyear}.csv", index=False) + # Add the print statement to the list. + print_statement_list.append(print_statement_lossyear) + # Add the file name to the list. + filename_list.append(f'"{file_name_lossyear}.csv"') + + # Add a legend to the plot. + ax.legend( + [Patch(facecolor=gain_color), Patch(facecolor=lossyear_color)], + [ + "Global forest cover \n gain 2000–2012", + f"Global forest cover \n loss {str(2000+start_year)}-{str(2000+end_year)}", + ], + loc="lower right", + bbox_to_anchor=(-0.1, 0.75), + frameon=False, + ) + + plt.title("Global Forest Change Layers") + plt.savefig(figure_fn) + plt.show() + print(*print_statement_list, sep="\n") + print(*filename_list, sep="\n\t") + print(f'\nFigure saved as "{figure_fn}"'); + +def plot_gfclayer(gfclayer_ds, start_year, end_year, gfclayer): + if gfclayer == "treecover2000": + plot_gfclayer_treecover2000(gfclayer_ds, gfclayer) + elif gfclayer == "lossyear": + plot_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer) + elif gfclayer == "gain": + plot_gfclayer_gain(gfclayer_ds, gfclayer) + elif gfclayer == "alllayers": + plot_gfclayer_all(gfclayer_ds, start_year, end_year) + +def update_map_layers(self): + """ + Updates map widget to add new basemap when selected + using menu options. + """ + # Clear data load parameters to trigger data reload. + self.gfclayer_ds = None + + # Remove all layers from the map_layers Layers Group. + self.map_layers.clear_layers() + # Add the selected basemap to the layer Group. + self.map_layers.add_layer(self.basemap) + +class forest_monitoring_app(HBox): + def __init__(self): + super().__init__() + + ################## + # HEADER FOR APP # + ################## + + # Create the header widget. + header_title_text = "

Digital Earth Africa Forest Change

" + instruction_text = """

Select the desired Global Forest Change layer, then zoom in and draw a polygon to + select an area for which to plot the selected Global Forest Change layer. Alternatively, upload a vector file of the area of interest.

""" + self.header = deawidgets.create_html( + value=f"{header_title_text}{instruction_text}" + ) + self.header.layout = make_box_layout() + + ############################ + # WIDGETS FOR APP CONTROLS # + ############################ + + ## Selection widget for selecting the basemap to use for the map widget. + ## and when plotting the Global Forest Change Layer. + # Basemaps available for selection for the map widget. + self.basemap_list = [ + ("Open Street Map", basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)), + ("ESRI World Imagery", basemap_to_tiles(basemaps.Esri.WorldImagery)), + ] + # Set the default basemap to be used for the map widget / initial value for the widget. + self.basemap = self.basemap_list[0][1] + # Dropdown selection widget. + dropdown_basemap = deawidgets.create_dropdown( + options=self.basemap_list, value=self.basemap + ) + # Register the update function to run when a new value is selected + # on the dropdown_basemap widget. + dropdown_basemap.observe(self.update_basemap, "value") + # Text to accompany the dropdown selection widget. + basemap_selection_html = deawidgets.create_html( + value=f"
Map overlay:" + ) + # Combine the basemap_selection_html text and the dropdown_basemap widget in a single container. + basemap_selection = VBox([basemap_selection_html, dropdown_basemap]) + + ## Selection widget for selecting the Global Forest change layer to plot. + # Global Forest Change layers available plotting. + self.gfclayers_list = [ + ("Year of gross forest cover loss event", "lossyear"), + ("Global forest cover gain 2000–2012", "gain"), + ("Tree canopy cover for the year 2000", "treecover2000"), + ("All layers", "alllayers"), + ] + # Set the default GFC layer to be plotted / initial value for the widget. + self.gfclayer = self.gfclayers_list[0][1] + + ## Selection widget for the data time range. + # Set the default time range for which to load data for. + self.start_year = 1 + self.end_year = 21 + + # Create the time range selector. + time_range = list(range(self.start_year, self.end_year + 1)) + time_range_str = [str(2000 + i) for i in time_range] + + timerange_options = tuple(zip(time_range_str, time_range)) + timerange_selection_slide = widgets.SelectionRangeSlider( + options=timerange_options, + value=(self.start_year, self.end_year), + description="", + disabled=False, + ) + # Register the update function to run when a new value is selected on the slider. + timerange_selection_slide.observe(self.update_timerange, "value") + # Text to accompany the timerange_selection widget. + timerange_selection_html = deawidgets.create_html( + value=f"
Forest Cover Loss Time Range:" + ) + # Combine the timerange_selection_text and the timerange_selection_slide in a single container. + timerange_selection = VBox( + [timerange_selection_html, timerange_selection_slide] + ) + + # Set the initial parameter for the GFC layer dataset. + self.gfclayer_ds = None + # Dropdown selection widget. + dropdown_gfclayer = deawidgets.create_dropdown( + options=self.gfclayers_list, value=self.gfclayer + ) + # Register the update function to run when a new value is selected + # on the dropdown_gfclayer widget. + dropdown_gfclayer.observe(self.update_gfclayer, "value") + # Text to accompany the dropdown selection widget. + gfclayer_selection_html = deawidgets.create_html( + value=f"
Global Forest Change Layer:" + ) + # Combine the gfclayer_selection_html text and the dropdown_gfclayer widget in a single container. + gfclayer_selection = VBox([gfclayer_selection_html, dropdown_gfclayer]) + + ## Add a checkbox for whether to overide the limit to the size of polygon drawn on the + ## map widget. + # Initial value of the widget. + self.max_size = False + # CheckBox widget. + checkbox_max_size = deawidgets.create_checkbox( + value=self.max_size, description="Enable", layout={"width": "95%"} + ) + # Text to accompany the CheckBox widget. + checkbox_max_size_html = deawidgets.create_html( + value=f"""
Override maximum size limit: + (use with caution; may cause memory issues/crashes)""" + ) + # Register the update function to run when the checkbox is ticked. + # on the checkbox_max_size CheckBox + checkbox_max_size.observe(self.update_checkbox_max_size, "value") + # # Combine the checkbox_max_size_html text and the checkbox_max_size widget in a single container. + enable_max_size = VBox([checkbox_max_size_html, checkbox_max_size]) + + # Add widget to enable uploading a geojson or ESRI shapefile. + self.gdf_uploaded = None + fileupload_aoi = widgets.FileUpload(accept="", multiple=True) + # Register the update function to be called for the file upload. + fileupload_aoi.observe(self.update_fileupload_aoi, "value") + fileupload_html = deawidgets.create_html(value=f"""
Advanced
Upload a GeoJSON or ESRI Shapefile (<5 mb) containing a single area of interest.
""") + fileupload = VBox([fileupload_html, fileupload_aoi]) + + + ## Put the app controls widgets into a single container. + parameter_selection = VBox( + [ + basemap_selection, + gfclayer_selection, + timerange_selection, + enable_max_size, + fileupload + ] + ) + parameter_selection.layout = make_box_layout() + + ## Button to click to run the app. + run_button = create_expanded_button( + description="Generate plot", button_style="info" + ) + # Register the update function to be called when the run_button button + # is clicked. + run_button.on_click(self.run_app) + + + + ########################### + # WIDGETS FOR APP OUTPUTS # + ########################### + + self.status_info = Output(layout=make_box_layout()) + self.output_plot = Output(layout=make_box_layout()) + + ################################# + # MAP WIDGET WITH DRAWING TOOLS # + ################################# + + # Create the map widget. + self.m = deawidgets.create_map( + map_center=(-18.45, 28.93), + zoom_level=11, + ) + self.m.layout = make_box_layout() + + # Create an empty Layer Group. + self.map_layers = LayerGroup(layers=()) + # Name of the Layer Group layer. + self.map_layers.name = "Map Overlays" + # Add the empty Layer Group as a single layer to the map widget. + self.m.add_layer(self.map_layers) + + # Create the desired drawing tools. + desired_drawtools = ["rectangle", "polygon"] + draw_control = deawidgets.create_drawcontrol(desired_drawtools) + # Add drawing tools to the map widget. + self.m.add_control(draw_control) + # Set the initial parameters for the drawing tools. + self.target = None + self.action = None + self.gdf_drawn = None + + ##################################### + # HANDLER FUNCTION FOR DRAW CONTROL # + ##################################### + + def handle_draw(target, action, geo_json): + + """ + Defines the action to take once something is drawn on the + map widget. + """ + # Remove previously uploaded data if present + self.gdf_uploaded = None + fileupload_aoi._counter = 0 + + self.target = target + self.action = action + + # Clear data load parameters to trigger data reload. + self.gfclayer_ds = None + + # Convert the drawn polygon geojson to a GeoDataFrame. + json_data = json.dumps(geo_json) + binary_data = json_data.encode() + io = BytesIO(binary_data) + io.seek(0) + gdf = gpd.read_file(io) + gdf.crs = "EPSG:4326" + + # Convert the GeoDataFrame to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute the area. + gdf_drawn_nsidc = gdf.copy().to_crs("EPSG:6933") + m2_per_ha = 10000 + area = gdf_drawn_nsidc.area.values[0] / m2_per_ha + + polyarea_label = ( + f"Total area of Global Forest Change {self.gfclayer} layer to load" + ) + polyarea_text = f"{polyarea_label}: {area:.2f} ha" + + # Test the size of the polygon drawn. + if self.max_size: + confirmation_text = """ + (Overriding maximum size limit; use with caution as may lead to memory issues)""" + self.header.value = ( + header_title_text + + instruction_text + + polyarea_text + + confirmation_text + ) + self.gdf_drawn = gdf + elif area <= 50000: + confirmation_text = """ + (Area to extract falls within + recommended 50000 ha limit)""" + self.header.value = ( + header_title_text + + instruction_text + + polyarea_text + + confirmation_text + ) + self.gdf_drawn = gdf + else: + warning_text = """ + (Area to extract is too large, + please select an area less than 50000 )""" + self.header.value = ( + header_title_text + instruction_text + polyarea_text + warning_text + ) + self.gdf_drawn = None + + # Register the handler for draw events. + draw_control.on_draw(handle_draw) + + ############################### + # SPECIFICATION OF APP LAYOUT # + ############################### + + # Create the app layout. + grid_rows = 12 + grid_columns = 11 + grid_height = "1500px" + grid_width = "auto" + grid = GridspecLayout( + grid_rows, grid_columns, height=grid_height, width=grid_width + ) + + # Place app widgets and components in app layout. + # [rows, columns] + grid[0, :] = self.header + grid[1:6, 0:4] = parameter_selection + grid[6, 0:4] = run_button + grid[7:, 0:4] = self.status_info + grid[6:, 4:] = self.output_plot + grid[1:6, 4:] = self.m + # Display using HBox children attribute + self.children = [grid] + + ###################################### + # DEFINITION OF ALL UPDATE FUNCTIONS # + ###################################### + + def update_basemap(self, change): + """ + Updates the basemap on the map widget based on the + selected value of the dropdown_basemap widget. + """ + self.basemap = change.new + self.output_plot_basemap = get_basemap(self.basemap.url) + update_map_layers(self) + + def update_gfclayer(self, change): + """ + Updates the Global Forest Change layer to be plotted + based on the selected value of the dropdown_gfclayer widget. + """ + self.gfclayer = change.new + + def update_timerange(self, change): + """Updates the time range of the data to be loaded""" + self.start_year = change.new[0] + self.end_year = change.new[1] + + def update_checkbox_max_size(self, change): + """ + Sets the value of self.max_size to True when the + checkbox_max_size CheckBox is checked. + """ + self.max_size = change.new + + def update_fileupload_aoi(self, change): + + # Clear any drawn data if present + self.gdf_drawn = None + + # Save to file + for uploaded_filename in change.new.keys(): + with open(uploaded_filename, "wb") as output_file: + content = change.new[uploaded_filename]['content'] + output_file.write(content) + + with self.status_info: + + try: + + print('Loading vector data...', end='\r') + valid_files = [ + file for file in change.new.keys() + if file.lower().endswith(('.shp', '.geojson')) + ] + valid_file = valid_files[0] + aoi_gdf = (gpd.read_file(valid_file).to_crs( + "EPSG:4326").explode().reset_index(drop=True)) + + # Create a geodata + geodata = GeoData(geo_dataframe=aoi_gdf, + style={ + 'color': 'black', + 'weight': 3 + }) + + # Add to map + xmin, ymin, xmax, ymax = aoi_gdf.total_bounds + self.m.fit_bounds([[ymin, xmin], [ymax, xmax]]) + self.m.add_layer(geodata) + + # If completed, add to attribute + self.gdf_uploaded = aoi_gdf + + except IndexError: + print( + "Cannot read uploaded files. Please ensure that data is " + "in either GeoJSON or ESRI Shapefile format.", + end='\r') + self.gdf_uploaded = None + + except fiona.errors.DriverError: + print( + "Shapefile is invalid. Please ensure that all shapefile " + "components (e.g. .shp, .shx, .dbf, .prj) are uploaded.", + end='\r') + self.gdf_uploaded = None + + def run_app(self, change): + + # Clear progress bar and output areas before running. + self.status_info.clear_output() + self.output_plot.clear_output() + + with self.status_info: + # Load the area of interest from the map or uploaded files. + if self.gdf_uploaded is not None: + aoi_gdf = self.gdf_uploaded + elif self.gdf_drawn is not None: + aoi_gdf = self.gdf_drawn + else: + print(f'No valid polygon drawn on the map or uploaded. Please draw a valid a transect on the map, or upload a GeoJSON or ESRI Shapefile.', + end='\r') + aoi_gdf = None + + # If valid area of interest data returned. Load the selected Global Forest Change data. + if aoi_gdf is not None: + + if self.gfclayer_ds is None: + if self.gfclayer != "alllayers": + self.gfclayer_ds = load_gfclayer(gdf_drawn=aoi_gdf, gfclayer=self.gfclayer) + else: + self.gfclayer_ds = load_all_gfclayers(gdf_drawn=aoi_gdf) + else: + print("Using previously loaded data") + + # Plot the selected Global Forest Change layer. + if self.gfclayer_ds is not None: + with self.output_plot: + plot_gfclayer(gfclayer_ds=self.gfclayer_ds, + start_year=self.start_year, + end_year=self.end_year, + gfclayer=self.gfclayer) + else: + with self.status_info: + print(f"No Global Forest Change {self.gfclayer} layer data found in the selected area. Please select a new polygon over an area with data.") \ No newline at end of file diff --git a/deafrica_tools/app/geomedian.py b/deafrica_tools/app/geomedian.py new file mode 100644 index 0000000..4c7f45e --- /dev/null +++ b/deafrica_tools/app/geomedian.py @@ -0,0 +1,126 @@ +""" +Geomedian widget: generates an interactive visualisation of +the geomedian summary statistic. +""" + +# Load modules +import ipywidgets as widgets +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +import numpy as np +import xarray as xr +from odc.algo import xr_geomedian + +def run_app(): + + """ + An interactive app that allows users to visualise the difference between the median and geomedian time-series summary statistics. By modifying the red-green-blue values of three timesteps for a given pixel, the user changes the output summary statistics. + + This allows a visual representation of the difference through the output values, RGB colour, as well as showing values plotted as a vector on a 3-dimensional space. + + Last modified: December 2021 + """ + + # Define the red-green-blue sliders for timestep 1 + p1r = widgets.IntSlider(description='Red', max=255, value=58) + p1g = widgets.IntSlider(description='Green', max=255, value=153) + p1b = widgets.IntSlider(description='Blue', max=255, value=68) + + # Define the red-green-blue sliders for timestep 2 + p2r = widgets.IntSlider(description='Red', max=255, value=208) + p2g = widgets.IntSlider(description='Green', max=255, value=221) + p2b = widgets.IntSlider(description='Blue', max=255, value=203) + + # Define the red-green-blue sliders for timestep 3 + p3r = widgets.IntSlider(description='Red', max=255, value=202) + p3g = widgets.IntSlider(description='Green', max=255, value=82) + p3b = widgets.IntSlider(description='Blue', max=255, value=33) + + # Define the median calculation for the timesteps + def f(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b): + print('Red Median = {}'.format(np.median([p1r, p2r, p3r]))) + print('Green Median = {}'.format(np.median([p1g, p2g, p3g]))) + print('Blue Median = {}'.format(np.median([p1b, p2b, p3b]))) + + # Define the geomedian calculation for the timesteps + def g(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b): + print('Red Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0])) + print('Green Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0])) + print('Blue Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0])) + + # Define the Timestep 1 box colour + def h(p1r, p1g, p1b): + fig1, axes1 = plt.subplots(figsize=(2,2)) + fig1 = plt.imshow([[(p1r, p1g, p1b)]]) + axes1.set_title('Timestep 1') + axes1.axis('off') + plt.show(fig1) + + # Define the Timestep 2 box colour + def hh(p2r, p2g, p2b): + fig2, axes2 = plt.subplots(figsize=(2,2)) + fig2 = plt.imshow([[(p2r, p2g, p2b)]]) + axes2.set_title('Timestep 2') + axes2.axis('off') + plt.show(fig2) + + # Define the Timestep 3 box colour + def hhh(p3r, p3g, p3b): + fig3, axes3 = plt.subplots(figsize=(2,2)) + fig3 = plt.imshow([[(p3r, p3g, p3b)]]) + axes3.set_title('Timestep 3') + axes3.axis('off') + plt.show(fig3) + + # Define the Median RGB colour box + def i(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b): + fig4, axes4 = plt.subplots(figsize=(3,3)) + fig4 = plt.imshow([[(int(np.median([p1r, p2r, p3r])), int(np.median([p1g, p2g, p3g])), int(np.median([p1b, p2b, p3b])))]]) + axes4.set_title('Median RGB - All timesteps') + axes4.axis('off') + plt.show(fig4) + + # Define the Geomedian RGB colour box + def ii(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b): + fig5, axes5 = plt.subplots(figsize=(3,3)) + fig5 = plt.imshow([[(int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0]), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0]), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0]))]]) + axes5.set_title('Geomedian RGB - All timesteps') + axes5.axis('off') + plt.show(fig5) + + # Define 3-D axis to display vectors on + def j(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b): + fig6 = plt.figure() + axes6 = fig6.add_subplot(111, projection='3d') + x = [p1r, p2r, p3r, int(np.median([p1r, p2r, p3r])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0])] + y = [p1g, p2g, p3g, int(np.median([p1g, p2g, p3g])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0])] + z = [p1b, p2b, p3b, int(np.median([p1b, p2b, p3b])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0])] + labels = [' 1', ' 2', ' 3', ' median', ' geomedian'] + axes6.scatter(x, y, z, c=['black','black','black','r', 'blue'], marker='o') + axes6.set_xlabel('Red') + axes6.set_ylabel('Green') + axes6.set_zlabel('Blue') + axes6.set_xlim3d(0, 255) + axes6.set_ylim3d(0, 255) + axes6.set_zlim3d(0, 255) + for ax, ay, az, label in zip(x, y, z, labels): + axes6.text(ax, ay, az, label) + plt.title('Each band represents a dimension.') + plt.show() + + # Define outputs + outf = widgets.interactive_output(f, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b}) + outg = widgets.interactive_output(g, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b}) + + outh = widgets.interactive_output(h, {'p1r': p1r, 'p1g': p1g, 'p1b': p1b}) + outhh = widgets.interactive_output(hh, {'p2r': p2r, 'p2g': p2g, 'p2b': p2b}) + outhhh = widgets.interactive_output(hhh, {'p3r': p3r, 'p3g': p3g, 'p3b': p3b}) + + outi = widgets.interactive_output(i, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b}) + outii = widgets.interactive_output(ii, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b}) + + outj = widgets.interactive_output(j, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b}) + + app_output = widgets.HBox([widgets.VBox([widgets.HBox([outh, widgets.VBox([ p1r, p1g, p1b])]), widgets.HBox([outhh, widgets.VBox([p2r, p2g, p2b])]), widgets.HBox([outhhh, widgets.VBox([ p3r, p3g, p3b])])]), widgets.VBox([widgets.HBox([widgets.VBox([outf, outi]), widgets.VBox([outg, outii])]), outj])]) + + return app_output \ No newline at end of file diff --git a/deafrica_tools/app/imageexport.py b/deafrica_tools/app/imageexport.py new file mode 100644 index 0000000..8b0cf1c --- /dev/null +++ b/deafrica_tools/app/imageexport.py @@ -0,0 +1,372 @@ +""" +Create an interactive map for selecting satellite imagery and exporting image files. +""" + +# Load modules +import datacube +import itertools +import numpy as np +import matplotlib.pyplot as plt +from odc.ui import select_on_a_map +from datacube.utils.geometry import CRS +from datacube.utils import masking +from skimage import exposure +from ipyleaflet import (WMSLayer, basemaps, basemap_to_tiles) +from traitlets import Unicode + +from deafrica_tools.spatial import reverse_geocode +from deafrica_tools.dask import create_local_dask_cluster + + +def select_region_app(date, + satellites, + size_limit=10000): + """ + An interactive app that allows the user to select a region from a + map using imagery from Sentinel-2 and Landsat. The output of this + function is used as the input to :func:`export_image_app` to export high- + resolution satellite images. + + Last modified: September 2021 + + Parameters + ---------- + date : str + The exact date used to plot imagery on the interactive map + (e.g. ``date='1988-01-01'``). + satellites : str + The satellite data to plot on the interactive map. The + following options are supported: + + ``'Landsat-9'``: data from the Landsat 9 satellite + ``'Landsat-8'``: data from the Landsat 8 satellite + ``'Landsat-7'``: data from the Landsat 7 satellite + ``'Landsat-5'``: data from the Landsat 5 satellite + ``'Sentinel-2'``: data from Sentinel-2A and Sentinel-2B + ``'Sentinel-2 geomedian'``: data from the Sentinel-2 annual geomedian + + size_limit : int, optional + An optional size limit for the area selection in sq km. + Defaults to 10000 sq km. + + Returns + ------- + A dictionary containing: + + * 'geopolygon' (defining the area to export imagery from), + * 'date' (date used to export imagery), and + * 'satellites' (the satellites from which to extract imagery). + + These are passed to the :func:`export_image_app` function to export the image. + """ + + ######################## + # Select and load data # + ######################## + + # Load DEA WMS + class TimeWMSLayer(WMSLayer): + time = Unicode('').tag(sync=True, o=True) + + # WMS layers + wms_params = { + 'Landsat-9': 'ls9_sr', + 'Landsat-8': 'ls8_sr', + 'Landsat-7': 'ls7_sr', + 'Landsat-5': 'ls5_sr', + 'Sentinel-2': 's2_l2a', + 'Sentinel-2 geomedian': 'gm_s2_annual' + } + + time_wms = TimeWMSLayer(url='https://ows.digitalearth.africa/', + layers=wms_params[satellites], + time=date, + format='image/png', + transparent=True, + attribution='Digital Earth Africa') + + # Plot interactive map to select area + basemap = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik) + geopolygon = select_on_a_map(height='1000px', + layers=( + basemap, + time_wms, + ), + center=(4, 20), + zoom=4) + + # Test size of selected area + area = geopolygon.to_crs(crs=CRS('epsg:6933')).area / 1000000 + if area > size_limit: + print(f'Warning: Your selected area is {area:.00f} sq km. ' + f'Please select an area of less than {size_limit} sq km.' + f'\nTo select a smaller area, re-run the cell ' + f'above and draw a new polygon.') + + else: + return {'geopolygon': geopolygon, + 'date': date, + 'satellites': satellites} + + +def export_image_app(geopolygon, + date, + satellites, + style='True colour', + resolution=None, + vmin=0, + vmax=2000, + percentile_stretch=None, + power=None, + image_proc_funcs=None, + output_format="jpg", + standardise_name=False): + """ + Exports Digital Earth Africa satellite data as an image file + based on the extent and time period selected using + :func:`select_region_app`. The function supports Sentinel-2 and Landsat + data, creating True and False colour images. + + By default, files are named using: + + ``" - - - .png"`` + + Set ``standardise_name=True`` for a machine-readable name: + + ``"___.png"`` + + Last modified: September 2021 + + Parameters + ---------- + geopolygon : datacube.utils.geometry object + A datacube geopolygon providing the spatial bounds used to load + satellite data. + date : str + The exact date used to extract imagery + (e.g. `date='1988-01-01'`). + satellites : str + The satellite data to be used to extract imagery. The + following options are supported: + + ``'Landsat-9'``: data from the Landsat 9 satellite + ``'Landsat-8'``: data from the Landsat 8 satellite + ``'Landsat-7'``: data from the Landsat 7 satellite + ``'Landsat-5'``: data from the Landsat 5 satellite + ``'Sentinel-2'``: data from Sentinel-2A and Sentinel-2B + ``'Sentinel-2 geomedian'``: data from the Sentinel-2 annual geomedian + + style : str, optional + The style used to produce the image. Two options are currently + supported: + + * ``'True colour'``: Creates a true colour image using the red, + green and blue satellite bands + * ``'False colour'``: Creates a false colour image using + short-wave infrared, infrared and green satellite bands. + The specific bands used vary between Landsat and Sentinel-2. + + resolution : tuple, optional + The spatial resolution to load data. By default, the tool will + automatically set the best possible resolution depending on the + satellites selected (i.e 30 m for Landsat, 10 m for Sentinel-2). + Increasing this (e.g. to ``resolution=(-100, 100)``) can be useful + for loading large spatial extents. + vmin, vmax : int or float + The minimum and maximum surface reflectance values used to + clip the resulting imagery to enhance contrast. + percentile_stretch : tuple of floats, optional + An tuple of two floats (between 0.00 and 1.00) that can be used + to clip the imagery to based on percentiles to get more control + over the brightness and contrast of the image. The default is + ``None``; ``(0.02, 0.98)`` is equivelent to ``robust=True``. If this + parameter is used, ``vmin`` and ``vmax`` will have no effect. + power : float, optional + Raises imagery by a power to reduce bright features and + enhance dark features. This can add extra definition over areas + with extremely bright features like snow, beaches or salt pans. + image_proc_funcs : list of funcs, optional + An optional list containing functions that will be applied to + the output image. This can include image processing functions + such as increasing contrast, unsharp masking, saturation etc. + The function should take AND return a `numpy.ndarray` with + shape ``[y, x, bands]``. If your function has parameters, you + can pass in custom values using a lambda function, e.g.: + ``[lambda x: skimage.filters.unsharp_mask(x, radius=5, amount=0.2)]`` + output_format : str, optional + The output file format of the image. Valid options include ``'jpg'`` + and ``'png'``. Defaults to ``'jpg'``. + standardise_name : bool, optional + Whether to export the image file with a machine-readable + file name (e.g. ``___.png``) + """ + + ########################### + # Set up satellite params # + ########################### + + sat_params = { + 'Landsat-9': { + 'products': ['ls9_sr'], + 'resolution': [-30, 30], + 'styles': { + 'True colour': ['red', 'green', 'blue'], + 'False colour': ['swir_1', 'nir', 'green'] + } + }, + 'Landsat-8': { + 'products': ['ls8_sr'], + 'resolution': [-30, 30], + 'styles': { + 'True colour': ['red', 'green', 'blue'], + 'False colour': ['swir_1', 'nir', 'green'] + } + }, + 'Landsat-7': { + 'products': ['ls7_sr'], + 'resolution': [-30, 30], + 'styles': { + 'True colour': ['red', 'green', 'blue'], + 'False colour': ['swir_1', 'nir', 'green'] + } + }, + 'Landsat-5': { + 'products': ['ls5_sr'], + 'resolution': [-30, 30], + 'styles': { + 'True colour': ['red', 'green', 'blue'], + 'False colour': ['swir_1', 'nir', 'green'] + } + }, + 'Sentinel-2': { + 'products': ['s2_l2a'], + 'resolution': [-10, 10], + 'styles': { + 'True colour': ['red', 'green', 'blue'], + 'False colour': ['swir_2', 'nir_1', 'green'] + } + }, + 'Sentinel-2 geomedian': { + 'products': ['gm_s2_annual'], + 'resolution': [-10, 10], + 'styles': { + 'True colour': ['red', 'green', 'blue'], + 'False colour': ['swir_2', 'nir_1', 'green'] + } + }, + } + + ############# + # Load data # + ############# + + # Connect to datacube database + dc = datacube.Datacube(app='Exporting_satellite_images') + + # Configure local dask cluster + client = create_local_dask_cluster(return_client=True) + + # Create query after adjusting interval time to UTC by + # adding a UTC offset of -10 hours. + start_date = np.datetime64(date) + query_params = { + 'time': (str(start_date)), + 'geopolygon': geopolygon + } + + # Find matching datasets + dss = [ + dc.find_datasets(product=i, **query_params) + for i in sat_params[satellites]['products'] + ] + dss = list(itertools.chain.from_iterable(dss)) + + # Get CRS and sensor + crs = str(dss[0].crs) + + if satellites == 'Sentinel-2 geomedian': + sensor = satellites + else: + sensor = dss[0].metadata_doc['properties']['eo:platform'].capitalize() + sensor = sensor[0:-1].replace('_', '-') + sensor[-1].capitalize() + + # Use resolution if provided, otherwise use default + if resolution: + sat_params[satellites]['resolution'] = resolution + + load_params = { + 'output_crs': crs, + 'resolution': sat_params[satellites]['resolution'], + 'resampling': 'bilinear' + } + + # Load data from datasets + ds = dc.load(datasets=dss, + measurements=sat_params[satellites]['styles'][style], + group_by='solar_day', + dask_chunks={ + 'time': 1, + 'x': 3000, + 'y': 3000 + }, + **load_params, + **query_params) + ds = masking.mask_invalid_data(ds) + + rgb_array = ds.isel(time=0).to_array().values + + ############ + # Plotting # + ############ + + # Create unique file name + centre_coords = geopolygon.centroid.coords[0][::-1] + site = reverse_geocode(coords=centre_coords) + fname = (f"{sensor} - {date} - {site} - {style}, " + f"{load_params['resolution'][1]} m resolution.{output_format}") + + # Remove spaces and commas if requested + if standardise_name: + fname = fname.replace(' - ', '_').replace(', ', + '-').replace(' ', + '-').lower() + + print( + f'\nExporting image to {fname}.\nThis may take several minutes to complete...' + ) + + # Convert to numpy array + rgb_array = np.transpose(rgb_array, axes=[1, 2, 0]) + + # If percentile stretch is supplied, calculate vmin and vmax + # from percentiles + if percentile_stretch: + vmin, vmax = np.nanpercentile(rgb_array, percentile_stretch) + + # Raise by power to dampen bright features and enhance dark. + # Raise vmin and vmax by same amount to ensure proper stretch + if power: + rgb_array = rgb_array**power + vmin, vmax = vmin**power, vmax**power + + # Rescale/stretch imagery between vmin and vmax + rgb_rescaled = exposure.rescale_intensity(rgb_array.astype(float), + in_range=(vmin, vmax), + out_range=(0.0, 1.0)) + + # Apply image processing funcs + if image_proc_funcs: + for i, func in enumerate(image_proc_funcs): + print(f'Applying custom function {i + 1}') + rgb_rescaled = func(rgb_rescaled) + + # Plot RGB + plt.imshow(rgb_rescaled) + + # Export to file + plt.imsave(fname=fname, arr=rgb_rescaled, format=output_format) + + # Close dask client + client.shutdown() + + print('Finished exporting image.') diff --git a/deafrica_tools/app/wetlandsinsighttool.py b/deafrica_tools/app/wetlandsinsighttool.py new file mode 100644 index 0000000..e8b6ce5 --- /dev/null +++ b/deafrica_tools/app/wetlandsinsighttool.py @@ -0,0 +1,388 @@ +""" +Wetlands insight tool widget, which can be used to run an interactive +version of the wetlands insight tool. +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import datacube +import warnings +import seaborn as sns +import matplotlib.pyplot as plt +from datacube.utils.geometry import CRS +from ipyleaflet import ( + WMSLayer, + basemaps, + basemap_to_tiles, + Map, + DrawControl, + WidgetControl, + LayerGroup, + LayersControl, +) +from traitlets import Unicode +from ipywidgets import ( + GridspecLayout, + Button, + Layout, + HBox, + VBox, + HTML, + Output, +) +import json +import geopandas as gpd +from io import BytesIO +from dask.diagnostics import ProgressBar + +import deafrica_tools +from deafrica_tools.dask import create_local_dask_cluster +from deafrica_tools.wetlands import WIT_drill +import deafrica_tools.app.widgetconstructors as deawidgets + + +def make_box_layout(): + return Layout( + #border='solid 1px black', + margin='0px 10px 10px 0px', + padding='5px 5px 5px 5px', + width='100%', + height='100%', + ) + + +def create_expanded_button(description, button_style): + return Button( + description=description, + button_style=button_style, + layout=Layout(width="auto", height="auto"), + ) + + +class wit_app(HBox): + def __init__(self, lang=None): + super().__init__() + + deafrica_tools.set_lang(lang) + + ########################################################## + # INITIAL ATTRIBUTES # + + self.startdate = "2020-01-01" + self.enddate = "2020-03-01" + self.mingooddata = 0.0 + self.resamplingfreq = "1M" + self.out_csv = "example_WIT.csv" + self.out_plot = "example_WIT.png" + self.product_list = [ + (_("None"), "none"), + (_("ESRI World Imagery"), "esri_world_imagery"), + (_("Sentinel-2 Geomedian"), "gm_s2_annual"), + (_("Water Observations from Space"), "wofs_ls_summary_annual"), + + ] + self.product = self.product_list[0][1] + self.product_year = "2020-01-01" + self.target = None + self.action = None + self.gdf_drawn = None + + ########################################################## + # HEADER FOR APP # + + # Create the Header widget + header_title_text = _("Wetlands Insight Tool") + instruction_text = _("Select parameters and AOI") + self.header = deawidgets.create_html(f"

{header_title_text}

{instruction_text}

") + self.header.layout = make_box_layout() + + ########################################################## + # HANDLER FUNCTION FOR DRAW CONTROL # + + # Define the action to take once something is drawn on the map + def update_geojson(target, action, geo_json): + + self.action = action + + json_data = json.dumps(geo_json) + binary_data = json_data.encode() + io = BytesIO(binary_data) + io.seek(0) + + gdf = gpd.read_file(io) + gdf.crs = "EPSG:4326" + self.gdf_drawn = gdf + + gdf_drawn_epsg6933 = gdf.copy().to_crs("EPSG:6933") + m2_per_km2 = 10 ** 6 + area = gdf_drawn_epsg6933.area.values[0] / m2_per_km2 + polyarea_label = _('Total polygon area') + polyarea_text = f"

{polyarea_label}: {area:.2f} km2

" + + if area <= 3000: + confirmation_text = '

' + _('Area falls within recommended limit') + '

' + self.header.value = header_title_text + polyarea_text + confirmation_text + else: + warning_text = '

' + _('Area is too large, please update your polygon') + '

' + self.header.value = header_title_text + polyarea_text + warning_text + + ########################################################## + # WIDGETS FOR APP OUTPUTS # + + self.dask_client = Output(layout=make_box_layout()) + self.progress_bar = Output(layout=make_box_layout()) + self.wit_plot = Output(layout=make_box_layout()) + self.progress_header = deawidgets.create_html("") + + ########################################################## + # MAP WIDGET, DRAWING TOOLS, WMS LAYERS # + + # Create drawing tools + desired_drawtools = ['rectangle', 'polygon'] + draw_control = deawidgets.create_drawcontrol(desired_drawtools) + + # Begin by displaying an empty layer group, and update the group with desired WMS on interaction. + self.deafrica_layers = LayerGroup(layers=()) + self.deafrica_layers.name = _('Map Overlays') + + # Create map widget + self.m = deawidgets.create_map() + + self.m.layout = make_box_layout() + + # Add tools to map widget + self.m.add_control(draw_control) + self.m.add_layer(self.deafrica_layers) + + # Store current basemap for future use + self.basemap = self.m.basemap + + ########################################################## + # WIDGETS FOR APP CONTROLS # + + # Create parameter widgets + startdate_picker = deawidgets.create_datepicker() + enddate_picker = deawidgets.create_datepicker() + min_good_data = deawidgets.create_boundedfloattext(self.mingooddata, 0.0, 1.0, 0.05) + resampling_freq = deawidgets.create_inputtext(self.resamplingfreq, self.resamplingfreq) + output_csv = deawidgets.create_inputtext(self.out_csv, self.out_csv) + output_plot = deawidgets.create_inputtext(self.out_plot, self.out_plot) + deaoverlay_dropdown = deawidgets.create_dropdown(self.product_list, self.product_list[0][1]) + run_button = create_expanded_button(_("Run"), "info") + + ########################################################## + # COLLECTION OF ALL APP CONTROLS # + + parameter_selection = VBox( + [ + HTML("" + _("Map Overlay:") + ""), + deaoverlay_dropdown, + HTML("" + _("Start Date:") + ""), + startdate_picker, + HTML("" + _("End Date:") + ""), + enddate_picker, + HTML("" + _("Minimum Good Data:") + ""), + min_good_data, + HTML("" + _("Resampling Frequency:") + ""), + resampling_freq, + HTML("" + _("Output CSV:") + ""), + output_csv, + HTML("" + _("Output Plot:") + ""), + output_plot, + ] + ) + parameter_selection.layout = make_box_layout() + + ########################################################## + # SPECIFICATION OF APP LAYOUT # + + # Create the layout #[rowspan, colspan] + grid = GridspecLayout(11, 10, height="1100px", width="auto") + + # Controls and Status + grid[0, :] = self.header + grid[1:6, 0:2] = parameter_selection + grid[6, 0:2] = run_button + + # Dask and Progress info + grid[1, 7:] = self.dask_client + grid[2:7, 7:] = self.progress_bar + + # Map + grid[1:7, 2:7] = self.m + + # Plot + grid[7:, :] = self.wit_plot + + # Display using HBox children attribute + self.children = [grid] + + ########################################################## + # SPECIFICATION UPDATE FUNCTIONS FOR EACH WIDGET # + + # Run update functions whenever various widgets are changed. + startdate_picker.observe(self.update_startdate, "value") + enddate_picker.observe(self.update_enddate, "value") + min_good_data.observe(self.update_mingooddata, "value") + resampling_freq.observe(self.update_resamplingfreq, "value") + output_csv.observe(self.update_outputcsv, "value") + output_plot.observe(self.update_outputplot, "value") + deaoverlay_dropdown.observe(self.update_deaoverlay, "value") + run_button.on_click(self.run_app) + draw_control.on_draw(update_geojson) + + ############################################################## + # DEFINITION OF ALL UPDATE FUNCTIONS # + + # set the start date to the new edited date + def update_startdate(self, change): + self.startdate = change.new + + # set the end date to the new edited date + def update_enddate(self, change): + self.enddate = change.new + + # set the min good data + def update_mingooddata(self, change): + self.mingooddata = change.new + + # set the resampling frequency + def update_resamplingfreq(self, change): + self.resamplingfreq = change.new + + # set the output csv + def update_outputcsv(self, change): + self.out_csv = change.new + + # set the output plot + def update_outputplot(self, change): + self.out_plot = change.new + + # Update product + def update_deaoverlay(self, change): + + self.product = change.new + + if self.product == "none": + self.deafrica_layers.clear_layers() + elif self.product == "esri_world_imagery": + self.deafrica_layers.clear_layers() + layer = basemap_to_tiles(basemaps.Esri.WorldImagery) + self.deafrica_layers.add_layer(layer) + else: + self.deafrica_layers.clear_layers() + layer = deawidgets.create_dea_wms_layer(self.product, self.product_year) + self.deafrica_layers.add_layer(layer) + + def run_app(self, change): + + # Clear progress bar and output areas before running + self.dask_client.clear_output() + self.progress_bar.clear_output() + self.wit_plot.clear_output() + + # Connect to datacube database + dc = datacube.Datacube(app="wetland_app") + + # Configure local dask cluster + with self.dask_client: + client = create_local_dask_cluster( + return_client=True, display_client=True + ) + + # Set any defaults + TCW_threshold = -0.035 + dask_chunks = dict(x=1000, y=1000, time=1) + + #check resampling freq + if self.resamplingfreq == 'None': + rsf = None + else: + rsf = self.resamplingfreq + + self.progress_header.value = f"

"+_("Progress")+"

" + + # run wetlands polygon drill + with self.progress_bar: +# with ProgressBar(): + warnings.filterwarnings("ignore") + try: + df = WIT_drill( + gdf=self.gdf_drawn, + time=(self.startdate, self.enddate), + min_gooddata=self.mingooddata, + resample_frequency=rsf, + TCW_threshold=TCW_threshold, + export_csv=self.out_csv, + dask_chunks=dask_chunks, + verbose=False, + verbose_progress=True, + ) + print(_("WIT complete")) + except AttributeError: + print(_("No polygon selected")) + + # close down the dask client + client.shutdown() + + # save the csv + if self.out_csv: + df.to_csv(self.out_csv, index_label="Datetime") + + # ---Plotting------------------------------ + + with self.wit_plot: + + fontsize = 17 + plt.rcParams.update({"font.size": fontsize}) + # set up color palette + pal = [ + sns.xkcd_rgb["cobalt blue"], + sns.xkcd_rgb["neon blue"], + sns.xkcd_rgb["grass"], + sns.xkcd_rgb["beige"], + sns.xkcd_rgb["brown"], + ] + + # make a stacked area plot + plt.close("all") + + fig, ax = plt.subplots(constrained_layout=True, figsize=(20, 6)) + + ax.stackplot( + df.index, + df.wofs_area_percent, + df.wet_percent, + df.green_veg_percent, + df.dry_veg_percent, + df.bare_soil_percent, + labels=[ + _("open water"), + _("wet"), + _("green veg"), + _("dry veg"), + _("bare soil"), + ], + colors=pal, + alpha=0.6, + ) + + # set axis limits to the min and max + ax.set_ylim(0, 100) + ax.set_xlim(df.index[0], df.index[-1]) + ax.tick_params(axis="x", labelsize=fontsize) + + # add a legend and a tight plot box + ax.legend(loc="lower left", framealpha=0.6) + ax.set_title(_("Percentage Fractional Cover, Wetness, and Water")) + # plt.tight_layout() + plt.show() + + if self.out_plot: + # save the figure + fig.savefig(f"{self.out_plot}") diff --git a/deafrica_tools/app/widgetconstructors.py b/deafrica_tools/app/widgetconstructors.py new file mode 100644 index 0000000..6881712 --- /dev/null +++ b/deafrica_tools/app/widgetconstructors.py @@ -0,0 +1,367 @@ +""" +Functions for easily defining widgets in the context of DE Africa notebooks. + +These are largely customised wrappers around existing widgets. +""" + +import ipyleaflet as leaflet +from ipyleaflet import LayersControl +import ipywidgets as widgets +from traitlets import Unicode + + +def create_datepicker(description='', value=None, layout={'width': '85%'}): + ''' + Create a DatePicker widget + + Last modified: July 2022 + + Parameters + ---------- + description : string + descirption label to attach + layout : dictionary + any layout commands for the widget + + Returns + ------- + date_picker : ipywidgets.widgets.widget_date.DatePicker + + ''' + + date_picker = widgets.DatePicker( + description=description, + layout=layout, + disabled=False, + value=value + ) + + return date_picker + + +def create_inputtext(value, placeholder, description="", layout={'width': '85%'}): + ''' + Create a Text widget + + Last modified: October 2021 + + Parameters + ---------- + value : string + initial value of the widget + placeholder : string + placeholder text to display to the user before intput + description : string + descirption label to attach + layout : dictionary + any layout commands for the widget + + Returns + ------- + input_text : ipywidgets.widgets.widget_string.Text + + ''' + + input_text = widgets.Text( + value=value, + placeholder=placeholder, + description=description, + layout=layout, + disabled=False + ) + + return input_text + + +def create_boundedfloattext(value, min_val, max_val, step_val, description="", layout={'width': '85%'}): + ''' + Create a BoundedFloatText widget + + Last modified: October 2021 + + Parameters + ---------- + value : float + initial value of the widget + min_val : float + minimum allowed value for the float + max_val : float + maximum allowed value for the float + step_val : float + allowed increment for the float + description : string + descirption label to attach + layout : dictionary + any layout commands for the widget + + Returns + ------- + float_text : ipywidgets.widgets.widget_float.BoundedFloatText + + ''' + + float_text = widgets.BoundedFloatText( + value=value, + min=min_val, + max=max_val, + step=step_val, + description=description, + layout=layout, + disabled=False, + ) + + return float_text + + +def create_dropdown(options, value, description="", layout={'width': '85%'}): + ''' + Create a Dropdown widget + + Last modified: October 2021 + + Parameters + ---------- + options : list + a list of options for the user to select from + value : string + initial value of the widget + description : string + descirption label to attach + layout : dictionary + any layout commands for the widget + + Returns + ------- + dropdown : ipywidgets.widgets.widget_selection.Dropdown + + ''' + + dropdown = widgets.Dropdown( + options=options, + value=value, + description=description, + layout=layout, + disabled=False, + ) + + return dropdown + + +def create_html(value): + ''' + Create a HTML widget + + Last modified: October 2021 + + Parameters + ---------- + value : string + HTML text to display + + Returns + ------- + html : ipywidgets.widgets.widget_string.HTML + + ''' + + html = widgets.HTML( + value=value, + ) + + return html + + +def create_map(map_center=(4, 20), zoom_level=3, basemap=leaflet.basemaps.OpenStreetMap.Mapnik, basemap_name='Open Street Map'): + ''' + Create an interactive ipyleaflet map + + Last modified: October 2021 + + Parameters + ---------- + map_center : tuple + A tuple containing the latitude and longitude to focus on. + Defaults to center of Africa, (4, 20) + zoom_level : integer + Zoom level for the map + Defaults to 3 to view all of Africa + basemap : ipyleaflet basemap (dict) + Basemap to use, can be any from https://ipyleaflet.readthedocs.io/en/latest/api_reference/basemaps.html + Defaults to Open Street Map (basemaps.OpenStreetMap.Mapnik) + basemap_name : string + Layer name for the basemap + + Returns + ------- + m : ipyleaflet.leaflet.Map + interactive ipyleaflet map + + ''' + + basemap_tiles = leaflet.basemap_to_tiles(basemap) + basemap_tiles.name = basemap_name + + m = leaflet.Map(center=map_center, zoom=zoom_level, basemap=basemap_tiles, scroll_wheel_zoom=True) + + return m + + +def create_dea_wms_layer(product, date): + ''' + Create a Digital Earth Africa WMS layer to add to a map + + Last modified: October 2021 + + Parameters + ---------- + product : string + The Digital Earth Africa product to load + (e.g. 'gm_s2_annual') + date : string (yyyy-mm-dd format) + The date to load the product for + + Returns + ------- + time_wms : ipyleaflet WMS layer + + ''' + + + # Load DEA WMS + class TimeWMSLayer(leaflet.WMSLayer): + time = Unicode("").tag(sync=True, o=True) + + time_wms = TimeWMSLayer( + url="https://ows.digitalearth.africa/", + layers=product, + time=date, + format="image/png", + transparent=True, + attribution="Digital Earth Africa", + ) + + return time_wms + + +def create_drawcontrol( + draw_controls = ['rectangle', 'polygon', 'circle', 'polyline', 'marker', 'circlemarker'], + rectangle_options={}, + polygon_options={}, + circle_options={}, + polyline_options={}, + marker_options={}, + circlemarker_options={}, +): + ''' + Create a draw control widget to add to ipyleaflet maps + + Last modified: October 2021 + + Parameters + ---------- + draw_controls : list + List of draw controls to add to the map. Defaults to adding all + Viable options are 'rectangle', 'polygon', 'circle', 'polyline', 'marker', 'circlemarker' + rectangle_options : dict + Options to customise the appearence of the relevant shape + User can supply, or leave blank to get default DE Africa appearence + polygon_options : dict + Options to customise the appearence of the relevant shape + User can supply, or leave blank to get default DE Africa appearence + circle_options : dict + Options to customise the appearence of the relevant shape + User can supply, or leave blank to get default DE Africa appearence + polyline_options : dict + Options to customise the appearence of the relevant shape + User can supply, or leave blank to get default DE Africa appearence + marker_options : dict + Options to customise the appearence of the relevant shape + User can supply, or leave blank to get default DE Africa appearence + circlemarker_options : dict + Options to customise the appearence of the relevant shape + User can supply, or leave blank to get default DE Africa appearence + + + Returns + ------- + draw_control : ipyleaflet.leaflet.DrawControl + + ''' + + # Set defualt DE Africa styling options for polygons + default_shapeoptions = { + "color": "#FFFFFF", + "opacity": 0.8, + "fillColor": "#336699", + "fillOpacity": 0.4, + } + default_drawerror = { + "color": "#FF6633", + "message": "Drawing error, clear all and try again" + } + + # Set draw control appearence to DE Africa defaults + # Do this if user has requested a control, but has not provided a corresponding options dict + + if ('rectangle' in draw_controls) and (not rectangle_options): + rectangle_options = {"shapeOptions": default_shapeoptions} + + if ('polygon' in draw_controls) and (not polygon_options): + polygon_options = { + "shapeOptions": default_shapeoptions, + "drawError": default_drawerror, + "allowIntersection": False, + } + + if ('circle' in draw_controls) and (not circle_options): + circle_options = {"shapeOptions": default_shapeoptions} + + if ('polyline' in draw_controls) and (not polyline_options): + polyline_options = {"shapeOptions": default_shapeoptions} + + if ('marker' in draw_controls) and (not marker_options): + marker_options = {'shapeOptions': {'opacity': 1.0}} + + if ('circlemarker' in draw_controls) and (not circlemarker_options): + circlemarker_options = {"shapeOptions": default_shapeoptions} + + # Instantiate draw control and add options + draw_control = leaflet.DrawControl() + draw_control.rectangle = rectangle_options + draw_control.polygon = polygon_options + draw_control.marker = marker_options + draw_control.circle = circle_options + draw_control.circlemarker = circlemarker_options + draw_control.polyline = polyline_options + + return draw_control + + +def create_checkbox(value, description="", layout={'width': '85%'}): + ''' + Create a Checkbox widget + + Last modified: July 2022 + + Parameters + ---------- + value : string + initial value of the widget; True or False + description : string + description label to attach + layout : dictionary + any layout commands for the widget + + Returns + ------- + dropdown : ipywidgets.widgets.widget_selection.Dropdown + + ''' + + checklist = widgets.Checkbox(value=value, + description=description, + layout=layout, + disabled=False, + indent=False) + + return checklist \ No newline at end of file diff --git a/deafrica_tools/areaofinterest.py b/deafrica_tools/areaofinterest.py new file mode 100644 index 0000000..ed69efa --- /dev/null +++ b/deafrica_tools/areaofinterest.py @@ -0,0 +1,54 @@ +""" +Function for defining an area of interest using either a point and buffer or a vector file. +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import geopandas as gpd +from shapely.geometry import box +from geojson import Feature, Point, FeatureCollection + +def define_area(lat=None, lon=None, buffer=None, vector_path=None): + ''' + Define an area of interest using either a point and buffer or a vector. + + Parameters: + ----------- + lat : float, optional + The latitude of the center point of the area of interest. + lon : float, optional + The longitude of the center point of the area of interest. + buffer : float, optional + The buffer around the center point, in degrees. + vector_path : str, optional + The path to a vector defining the area of interest. + + Returns: + -------- + feature_collection : dict + A GeoJSON feature collection representing the area of interest. + ''' + # Define area using point and buffer + if lat is not None and lon is not None and buffer is not None: + lat_range = (lat - buffer, lat + buffer) + lon_range = (lon - buffer, lon + buffer) + box_geom = box(min(lon_range), min(lat_range), max(lon_range), max(lat_range)) + aoi = gpd.GeoDataFrame(geometry=[box_geom], crs='EPSG:4326') + + # Define area using vector + elif vector_path is not None: + aoi = gpd.read_file(vector_path).to_crs("EPSG:4326") + # If neither option is provided, raise an error + else: + raise ValueError("Either lat/lon/buffer or vector_path must be provided.") + + # Convert the GeoDataFrame to a GeoJSON FeatureCollection + features = [Feature(geometry=row["geometry"], properties=row.drop("geometry").to_dict()) for _, row in aoi.iterrows()] + feature_collection = FeatureCollection(features) + + return feature_collection \ No newline at end of file diff --git a/deafrica_tools/bandindices.py b/deafrica_tools/bandindices.py new file mode 100644 index 0000000..0092925 --- /dev/null +++ b/deafrica_tools/bandindices.py @@ -0,0 +1,615 @@ +""" +Functions for computing remote sensing band indices on Digital Earth Africa +data. +""" + +# Import required packages +import warnings +import numpy as np + +# Define custom functions +def calculate_indices( + ds, + index=None, + collection=None, + satellite_mission=None, + custom_varname=None, + normalise=True, + drop=False, + deep_copy=True, +): + """ + Takes an xarray dataset containing spectral bands, calculates one of + a set of remote sensing indices, and adds the resulting array as a + new variable in the original dataset. + + Last modified: July 2022 + + Parameters + ---------- + ds : xarray Dataset + A two-dimensional or multi-dimensional array with containing the + spectral bands required to calculate the index. These bands are + used as inputs to calculate the selected water index. + + index : str or list of strs + A string giving the name of the index to calculate or a list of + strings giving the names of the indices to calculate: + + * ``'ASI'`` (Artificial Surface Index, Yongquan Zhao & Zhe Zhu 2022) + * ``'AWEI_ns'`` (Automated Water Extraction Index, no shadows, Feyisa 2014) + * ``'AWEI_sh'`` (Automated Water Extraction Index, shadows, Feyisa 2014) + * ``'BAEI'`` (Built-Up Area Extraction Index, Bouzekri et al. 2015) + * ``'BAI'`` (Burn Area Index, Martin 1998) + * ``'BSI'`` (Bare Soil Index, Rikimaru et al. 2002) + * ``'BUI'`` (Built-Up Index, He et al. 2010) + * ``'CMR'`` (Clay Minerals Ratio, Drury 1987) + * ``'ENDISI'`` (Enhanced Normalised Difference for Impervious Surfaces Index, Chen et al. 2019) + * ``'EVI'`` (Enhanced Vegetation Index, Huete 2002) + * ``'FMR'`` (Ferrous Minerals Ratio, Segal 1982) + * ``'IOR'`` (Iron Oxide Ratio, Segal 1982) + * ``'LAI'`` (Leaf Area Index, Boegh 2002) + * ``'MBI'`` (Modified Bare Soil Index, Nguyen et al. 2021) + * ``'MNDWI'`` (Modified Normalised Difference Water Index, Xu 1996) + * ``'MSAVI'`` (Modified Soil Adjusted Vegetation Index, Qi et al. 1994) + * ``'NBI'`` (New Built-Up Index, Jieli et al. 2010) + * ``'NBR'`` (Normalised Burn Ratio, Lopez Garcia 1991) + * ``'NDBI'`` (Normalised Difference Built-Up Index, Zha 2003) + * ``'NDCI'`` (Normalised Difference Chlorophyll Index, Mishra & Mishra, 2012) + * ``'NDMI'`` (Normalised Difference Moisture Index, Gao 1996) + * ``'NDSI'`` (Normalised Difference Snow Index, Hall 1995) + * ``'NDTI'`` (Normalised Difference Turbidity Index, Lacaux et al. 2007) + * ``'NDVI'`` (Normalised Difference Vegetation Index, Rouse 1973) + * ``'NDWI'`` (Normalised Difference Water Index, McFeeters 1996) + * ``'SAVI'`` (Soil Adjusted Vegetation Index, Huete 1988) + * ``'TCB'`` (Tasseled Cap Brightness, Crist 1985) + * ``'TCG'`` (Tasseled Cap Greeness, Crist 1985) + * ``'TCW'`` (Tasseled Cap Wetness, Crist 1985) + * ``'WI'`` (Water Index, Fisher 2016) + + collection : str + Deprecated in version 0.1.7. Use `satellite_mission` instead. + + Valid options are: + * ``'c2'`` (for USGS Landsat Collection 2) + If 'c2', then `satellite_mission='ls'`. + * ``'s2'`` (for Sentinel-2) + If 's2', then `satellite_mission='s2'`. + + satellite_mission : str + An string that tells the function which satellite mission's data is + being used to calculate the index. This is necessary because + different satellite missions use different names for bands covering + a similar spectra. + + Valid options are: + + * ``'ls'`` (for USGS Landsat) + * ``'s2'`` (for Copernicus Sentinel-2) + + custom_varname : str, optional + By default, the original dataset will be returned with + a new index variable named after `index` (e.g. 'NDVI'). To + specify a custom name instead, you can supply e.g. + `custom_varname='custom_name'`. Defaults to None, which uses + `index` to name the variable. + + normalise : bool, optional + Some coefficient-based indices (e.g. ``'WI'``, ``'BAEI'``, + ``'AWEI_ns'``, ``'AWEI_sh'``, ``'TCW'``, ``'TCG'``, ``'TCB'``, + ``'EVI'``, ``'LAI'``, ``'SAVI'``, ``'MSAVI'``) + produce different results if surface reflectance values are not + scaled between 0.0 and 1.0 prior to calculating the index. + Setting `normalise=True` first scales values to a 0.0-1.0 range + by dividing by 10000.0. Defaults to True. + + drop : bool, optional + Provides the option to drop the original input data, thus saving + space. If `drop=True`, returns only the index and its values. + + deep_copy: bool, optional + If `deep_copy=False`, calculate_indices will modify the original + array, adding bands to the input dataset and not removing them. + If the calculate_indices function is run more than once, variables + may be dropped incorrectly producing unexpected behaviour. This is + a bug and may be fixed in future releases. This is only a problem + when `drop=True`. + + Returns + ------- + ds : xarray Dataset + The original xarray Dataset inputted into the function, with a + new varible containing the remote sensing index as a DataArray. + If drop = True, the new variable/s as DataArrays in the + original Dataset. + """ + + # Set ds equal to a copy of itself in order to prevent the function + # from editing the input dataset. This is to prevent unexpected + # behaviour though it uses twice as much memory. + if deep_copy: + ds = ds.copy(deep=True) + + # Capture input band names in order to drop these if drop=True + if drop: + bands_to_drop = list(ds.data_vars) + print(f"Dropping bands {bands_to_drop}") + + # Dictionary containing remote sensing index band recipes + index_dict = { + # Normalised Difference Vegation Index, Rouse 1973 + "NDVI": lambda ds: (ds.nir - ds.red) / (ds.nir + ds.red), + # Enhanced Vegetation Index, Huete 2002 + "EVI": lambda ds: ( + 2.5 * ((ds.nir - ds.red) / (ds.nir + 6 * ds.red - 7.5 * ds.blue + 1)) + ), + # Leaf Area Index, Boegh 2002 + "LAI": lambda ds: ( + 3.618 + * ((2.5 * (ds.nir - ds.red)) / (ds.nir + (6 * ds.red) - (7.5 * ds.blue) + 1)) + - 0.118 + ), + # Soil Adjusted Vegetation Index, Huete 1988 + "SAVI": lambda ds: ((1.5 * (ds.nir - ds.red)) / (ds.nir + ds.red + 0.5)), + # Mod. Soil Adjusted Vegetation Index, Qi et al. 1994 + "MSAVI": lambda ds: ( + (2 * ds.nir + 1 - ((2 * ds.nir + 1) ** 2 - 8 * (ds.nir - ds.red)) ** 0.5) + / 2 + ), + # Normalised Difference Moisture Index, Gao 1996 + "NDMI": lambda ds: (ds.nir - ds.swir_1) / (ds.nir + ds.swir_1), + # Normalised Burn Ratio, Lopez Garcia 1991 + "NBR": lambda ds: (ds.nir - ds.swir_2) / (ds.nir + ds.swir_2), + # Burn Area Index, Martin 1998 + "BAI": lambda ds: (1.0 / ((0.10 - ds.red) ** 2 + (0.06 - ds.nir) ** 2)), + # Normalised Difference Chlorophyll Index, + # (Mishra & Mishra, 2012) + "NDCI": lambda ds: (ds.red_edge_1 - ds.red) / (ds.red_edge_1 + ds.red), + # Normalised Difference Snow Index, Hall 1995 + "NDSI": lambda ds: (ds.green - ds.swir_1) / (ds.green + ds.swir_1), + # Normalised Difference Water Index, McFeeters 1996 + "NDWI": lambda ds: (ds.green - ds.nir) / (ds.green + ds.nir), + # Modified Normalised Difference Water Index, Xu 2006 + "MNDWI": lambda ds: (ds.green - ds.swir_1) / (ds.green + ds.swir_1), + # Normalised Difference Built-Up Index, Zha 2003 + "NDBI": lambda ds: (ds.swir_1 - ds.nir) / (ds.swir_1 + ds.nir), + # Built-Up Index, He et al. 2010 + "BUI": lambda ds: ((ds.swir_1 - ds.nir) / (ds.swir_1 + ds.nir)) + - ((ds.nir - ds.red) / (ds.nir + ds.red)), + # Built-up Area Extraction Index, Bouzekri et al. 2015 + "BAEI": lambda ds: (ds.red + 0.3) / (ds.green + ds.swir_1), + # New Built-up Index, Jieli et al. 2010 + "NBI": lambda ds: (ds.swir_1 + ds.red) / ds.nir, + # Bare Soil Index, Rikimaru et al. 2002 + "BSI": lambda ds: ((ds.swir_1 + ds.red) - (ds.nir + ds.blue)) + / ((ds.swir_1 + ds.red) + (ds.nir + ds.blue)), + # Automated Water Extraction Index (no shadows), Feyisa 2014 + "AWEI_ns": lambda ds: ( + 4 * (ds.green - ds.swir_1) - (0.25 * ds.nir * +2.75 * ds.swir_2) + ), + # Automated Water Extraction Index (shadows), Feyisa 2014 + "AWEI_sh": lambda ds: ( + ds.blue + 2.5 * ds.green - 1.5 * (ds.nir + ds.swir_1) - 0.25 * ds.swir_2 + ), + # Water Index, Fisher 2016 + "WI": lambda ds: ( + 1.7204 + + 171 * ds.green + + 3 * ds.red + - 70 * ds.nir + - 45 * ds.swir_1 + - 71 * ds.swir_2 + ), + # Tasseled Cap Wetness, Crist 1985 + "TCW": lambda ds: ( + 0.0315 * ds.blue + + 0.2021 * ds.green + + 0.3102 * ds.red + + 0.1594 * ds.nir + + -0.6806 * ds.swir_1 + + -0.6109 * ds.swir_2 + ), + # Tasseled Cap Greeness, Crist 1985 + "TCG": lambda ds: ( + -0.1603 * ds.blue + + -0.2819 * ds.green + + -0.4934 * ds.red + + 0.7940 * ds.nir + + -0.0002 * ds.swir_1 + + -0.1446 * ds.swir_2 + ), + # Tasseled Cap Brightness, Crist 1985 + "TCB": lambda ds: ( + 0.2043 * ds.blue + + 0.4158 * ds.green + + 0.5524 * ds.red + + 0.5741 * ds.nir + + 0.3124 * ds.swir_1 + + -0.2303 * ds.swir_2 + ), + # Clay Minerals Ratio, Drury 1987 + "CMR": lambda ds: (ds.swir_1 / ds.swir_2), + # Ferrous Minerals Ratio, Segal 1982 + "FMR": lambda ds: (ds.swir_1 / ds.nir), + # Iron Oxide Ratio, Segal 1982 + "IOR": lambda ds: (ds.red / ds.blue), + # Normalized Difference Turbidity Index, Lacaux, J.P. et al. 2007 + "NDTI": lambda ds: (ds.red - ds.green) / (ds.red + ds.green), + # Modified Bare Soil Index, Nguyen et al. 2021 + "MBI": lambda ds: ((ds.swir_1 - ds.swir_2 - ds.nir) / (ds.swir_1 + ds.swir_2 + ds.nir)) + 0.5, + } + + # Enhanced Normalised Difference Impervious Surfaces Index, Chen et al. 2019 + def mndwi(ds): + return (ds.green - ds.swir_1) / (ds.green + ds.swir_1) + def swir_diff(ds): + return ds.swir_1/ds.swir_2 + def alpha(ds): + return (2*(np.mean(ds.blue)))/(np.mean(swir_diff(ds)) + np.mean(mndwi(ds)**2)) + def ENDISI(ds): + m = mndwi(ds) + s = swir_diff(ds) + a = alpha(ds) + return (ds.blue - (a)*(s + m**2))/(ds.blue + (a)*(s + m**2)) + + index_dict["ENDISI"] = ENDISI + + ## Artificial Surface Index, Yongquan Zhao & Zhe Zhu 2022 + def af(ds): + AF = (ds.nir - ds.blue) / (ds.nir + ds.blue) + AF_norm = (AF - AF.min(dim=["y","x"]))/(AF.max(dim=["y","x"]) - AF.min(dim=["y","x"])) + return AF_norm + def ndvi(ds): + return (ds.nir - ds.red) / (ds.nir + ds.red) + def msavi(ds): + return ((2 * ds.nir + 1 - ((2 * ds.nir + 1) ** 2 - 8 * (ds.nir - ds.red)) ** 0.5) / 2 ) + def vsf(ds): + NDVI = ndvi(ds) + MSAVI = msavi(ds) + VSF = 1 - NDVI * MSAVI + VSF_norm = (VSF - VSF.min(dim=["y","x"]))/(VSF.max(dim=["y","x"]) - VSF.min(dim=["y","x"])) + return VSF_norm + def mbi(ds): + return ((ds.swir_1 - ds.swir_2 - ds.nir) / (ds.swir_1 + ds.swir_2 + ds.nir)) + 0.5 + def embi(ds): + MBI = mbi(ds) + MNDWI = mndwi(ds) + return (MBI - MNDWI - 0.5) / (MBI + MNDWI + 1.5) + def ssf(ds): + EMBI = embi(ds) + SSF = 1 - EMBI + SSF_norm = (SSF - SSF.min(dim=["y","x"]))/(SSF.max(dim=["y","x"]) - SSF.min(dim=["y","x"])) + return SSF_norm + # Overall modulation using the Modulation Factor (MF). + def mf(ds): + MF = ((ds.blue + ds.green) - (ds.nir + ds.swir_1)) / ((ds.blue + ds.green) + (ds.nir + ds.swir_1)) + MF_norm = (MF - MF.min(dim=["y","x"]))/(MF.max(dim=["y","x"]) - MF.min(dim=["y","x"])) + return MF_norm + def ASI(ds): + AF = af(ds) + VSF = vsf(ds) + SSF = ssf(ds) + MF = mf(ds) + return AF * VSF * SSF * MF + + index_dict["ASI"] = ASI + + # If index supplied is not a list, convert to list. This allows us to + # iterate through either multiple or single indices in the loop below + indices = index if isinstance(index, list) else [index] + + # calculate for each index in the list of indices supplied (indexes) + for index in indices: + + # Select an index function from the dictionary + index_func = index_dict.get(str(index)) + + # If no index is provided or if no function is returned due to an + # invalid option being provided, raise an exception informing user to + # choose from the list of valid options + if index is None: + + raise ValueError( + f"No remote sensing `index` was provided. Please " + "refer to the function \ndocumentation for a full " + "list of valid options for `index` (e.g. 'NDVI')" + ) + + elif ( + index + in [ + "WI", + "BAEI", + "AWEI_ns", + "AWEI_sh", + "EVI", + "LAI", + "SAVI", + "MSAVI", + ] + and not normalise + ): + + warnings.warn( + f"\nA coefficient-based index ('{index}') normally " + "applied to surface reflectance values in the \n" + "0.0-1.0 range was applied to values in the 0-10000 " + "range. This can produce unexpected results; \nif " + "required, resolve this by setting `normalise=True`" + ) + + elif index_func is None: + + raise ValueError( + f"The selected index '{index}' is not one of the " + "valid remote sensing index options. \nPlease " + "refer to the function documentation for a full " + "list of valid options for `index`" + ) + + # Deprecation warning if `collection` is specified instead of `satellite_mission`. + if collection is not None: + warnings.warn('`collection` was deprecated in version 0.1.7. Use `satelite_mission` instead.', + DeprecationWarning, + stacklevel=2) + # Map the collection values to the valid satellite_mission values. + if collection == "c2": + satellite_mission = "ls" + elif collection == "s2": + satellite_mission = "s2" + # Raise error if no valid collection name is provided: + else: + raise ValueError( + f"'{collection}' is not a valid option for " + "`collection`. Please specify either \n" + "'c2' or 's2'.") + + + # Rename bands to a consistent format if depending on what satellite mission + # is specified in `satellite_mission`. This allows the same index calculations + # to be applied to all satellite missions. If no satellite mission was provided, + # raise an exception. + if satellite_mission is None: + + raise ValueError( + "No `satellite_mission` was provided. Please specify " + "either 'ls' or 's2' to ensure the \nfunction " + "calculates indices using the correct spectral " + "bands." + ) + + elif satellite_mission == "ls": + sr_max = 1.0 + # Dictionary mapping full data names to simpler alias names + # This only applies to properly-scaled "ls" data i.e. from + # the Landsat geomedians. calculate_indices will not show + # correct output for raw (unscaled) Landsat data (i.e. default + # outputs from dc.load) + bandnames_dict = { + "SR_B1": "blue", + "SR_B2": "green", + "SR_B3": "red", + "SR_B4": "nir", + "SR_B5": "swir_1", + "SR_B7": "swir_2", + } + + # Rename bands in dataset to use simple names (e.g. 'red') + bands_to_rename = { + a: b for a, b in bandnames_dict.items() if a in ds.variables + } + + elif satellite_mission == "s2": + sr_max = 10000 + # Dictionary mapping full data names to simpler alias names + bandnames_dict = { + "nir_1": "nir", + "B02": "blue", + "B03": "green", + "B04": "red", + "B05": "red_edge_1", + "B06": "red_edge_2", + "B07": "red_edge_3", + "B08": "nir", + "B11": "swir_1", + "B12": "swir_2", + } + + # Rename bands in dataset to use simple names (e.g. 'red') + bands_to_rename = { + a: b for a, b in bandnames_dict.items() if a in ds.variables + } + + # Raise error if no valid satellite_mission name is provided: + else: + raise ValueError( + f"'{satellite_mission}' is not a valid option for " + "`satellite_mission`. Please specify either \n" + "'ls' or 's2'" + ) + + # Apply index function + try: + # If normalised=True, divide data by 10,000 before applying func + mult = sr_max if normalise else 1.0 + index_array = index_func(ds.rename(bands_to_rename) / mult) + + except AttributeError: + raise ValueError( + f"Please verify that all bands required to " + f"compute {index} are present in `ds`." + ) + + # Add as a new variable in dataset + output_band_name = custom_varname if custom_varname else index + ds[output_band_name] = index_array + + # Once all indexes are calculated, drop input bands if drop=True + if drop: + ds = ds.drop(bands_to_drop) + + # Return input dataset with added water index variable + return ds + +def dualpol_indices( + ds, + co_pol='vv', + cross_pol='vh', + index=None, + custom_varname=None, + drop=False, + deep_copy=True, +): + """ + Takes an xarray dataset containing dual-polarization radar backscatter, + calculates one or a set of indices, and adds the resulting array as a + new variable in the original dataset. + + Last modified: July 2021 + + Parameters + ---------- + ds : xarray Dataset + A two-dimensional or multi-dimensional array containing the + two polarization bands. + + co_pol: str + Measurement name for the co-polarization band. + Default is 'vv' for Sentinel-1. + + cross_pol: str + Measurement name for the cross-polarization band. + Default is 'vh' for Sentinel-1. + + index : str or list of strs + A string giving the name of the index to calculate or a list of + strings giving the names of the indices to calculate: + + * ``'RVI'`` (Radar Vegetation Index for dual-pol, Trudel et al. 2012; Nasirzadehdizaji et al., 2019; Gururaj et al., 2019) + * ``'VDDPI'`` (Vertical dual depolarization index, Periasamy 2018) + * ``'theta'`` (pseudo scattering-type, Bhogapurapu et al. 2021) + * ``'entropy'`` (pseudo scattering entropy, Bhogapurapu et al. 2021) + * ``'purity'`` (co-pol purity, Bhogapurapu et al. 2021) + * ``'ratio'`` (cross-pol/co-pol ratio) + + custom_varname : str, optional + By default, the original dataset will be returned with + a new index variable named after `index` (e.g. 'RVI'). To + specify a custom name instead, you can supply e.g. + `custom_varname='custom_name'`. Defaults to None, which uses + `index` to name the variable. + + drop : bool, optional + Provides the option to drop the original input data, thus saving + space. If `drop=True`, returns only the index and its values. + + deep_copy: bool, optional + If `deep_copy=False`, calculate_indices will modify the original + array, adding bands to the input dataset and not removing them. + If the calculate_indices function is run more than once, variables + may be dropped incorrectly producing unexpected behaviour. This is + a bug and may be fixed in future releases. This is only a problem + when `drop=True`. + + Returns + ------- + ds : xarray Dataset + The original xarray Dataset inputted into the function, with a + new varible containing the remote sensing index as a DataArray. + If drop = True, the new variable/s as DataArrays in the + original Dataset. + """ + + if not co_pol in list(ds.data_vars): + raise ValueError(f"{co_pol} measurement is not in the dataset") + if not cross_pol in list(ds.data_vars): + raise ValueError(f"{cross_pol} measurement is not in the dataset") + + # Set ds equal to a copy of itself in order to prevent the function + # from editing the input dataset. This is to prevent unexpected + # behaviour though it uses twice as much memory. + if deep_copy: + ds = ds.copy(deep=True) + + # Capture input band names in order to drop these if drop=True + if drop: + bands_to_drop = list(ds.data_vars) + print(f"Dropping bands {bands_to_drop}") + + def ratio(ds): + return ds[cross_pol] / ds[co_pol] + + def purity(ds): + return (1 - ratio(ds)) / (1 + ratio(ds)) + + def theta(ds): + return np.arctan((1 - ratio(ds))**2 / (1 + ratio(ds)**2 - ratio(ds))) + + def P1(ds): + return 1 / (1 + ratio(ds)) + + def P2(ds): + return 1 - P1(ds) + + def entropy(ds): + return P1(ds)*np.log2(P1(ds)) + P2(ds)*np.log2(P2(ds)) + + # Dictionary containing remote sensing index band recipes + index_dict = { + # Radar Vegetation Index for dual-pol, Trudel et al. 2012 + "RVI": lambda ds: 4*ds[cross_pol] / (ds[co_pol] + ds[cross_pol]), + # Vertical dual depolarization index, Periasamy 2018 + "VDDPI": lambda ds: (ds[co_pol] + ds[cross_pol]) / ds[co_pol], + # cross-pol/co-pol ratio + "ratio": ratio, + # co-pol purity, Bhogapurapu et al. 2021 + "purity": purity, + # pseudo scattering-type, Bhogapurapu et al. 2021 + "theta": theta, + # pseudo scattering entropy, Bhogapurapu et al. 2021 + "entropy": entropy, + } + + # If index supplied is not a list, convert to list. This allows us to + # iterate through either multiple or single indices in the loop below + indices = index if isinstance(index, list) else [index] + + # calculate for each index in the list of indices supplied (indexes) + for index in indices: + + # Select an index function from the dictionary + index_func = index_dict.get(str(index)) + + # If no index is provided or if no function is returned due to an + # invalid option being provided, raise an exception informing user to + # choose from the list of valid options + if index is None: + + raise ValueError( + f"No radar `index` was provided. Please " + "refer to the function \ndocumentation for a full " + "list of valid options for `index` (e.g. 'RVI')" + ) + + elif index_func is None: + + raise ValueError( + f"The selected index '{index}' is not one of the " + "valid remote sensing index options. \nPlease " + "refer to the function documentation for a full " + "list of valid options for `index`" + ) + + # Apply index function + index_array = index_func(ds) + + # Add as a new variable in dataset + output_band_name = custom_varname if custom_varname else index + ds[output_band_name] = index_array + + # Once all indexes are calculated, drop input bands if drop=True + if drop: + ds = ds.drop(bands_to_drop) + + # Return input dataset with added water index variable + return ds diff --git a/deafrica_tools/classification.py b/deafrica_tools/classification.py new file mode 100644 index 0000000..027ba95 --- /dev/null +++ b/deafrica_tools/classification.py @@ -0,0 +1,1674 @@ +""" +Machine learning functions for classification of remote sensing data contained +in an Open Data Cube instance. +""" + +import multiprocessing as mp +import os +import sys +import time +import warnings +from abc import ABCMeta, abstractmethod +from copy import deepcopy + +import dask.array as da +import dask.distributed as dd +import joblib +import numpy as np +import pandas as pd +import xarray as xr +from dask_ml.wrappers import ParallelPostFit +from datacube.utils import geometry +from datacube.utils.geometry import assign_crs +from deafrica_tools.spatial import xr_rasterize +from sklearn.base import ClusterMixin +from sklearn.cluster import AgglomerativeClustering +from sklearn.cluster import KMeans +from sklearn.mixture import GaussianMixture +from sklearn.model_selection import BaseCrossValidator +from sklearn.model_selection import KFold, ShuffleSplit +from sklearn.utils import check_random_state +from tqdm.auto import tqdm + + +def sklearn_flatten(input_xr): + """ + Reshape a DataArray or Dataset with spatial (and optionally + temporal) structure into an np.array with the spatial and temporal + dimensions flattened into one dimension. + + This flattening procedure enables DataArrays and Datasets to be used + to train and predict with sklearn models. + + Last modified: September 2019 + + Parameters + ---------- + input_xr : xarray.DataArray or xarray.Dataset + Must have dimensions 'x' and 'y', may have dimension 'time'. + Dimensions other than 'x', 'y' and 'time' are unaffected by the + flattening. + + Returns + ---------- + input_np : numpy.array + A numpy array corresponding to input_xr.data (or + input_xr.to_array().data), with dimensions 'x','y' and 'time' + flattened into a single dimension, which is the first axis of + the returned array. input_np contains no NaNs. + + """ + # cast input Datasets to DataArray + if isinstance(input_xr, xr.Dataset): + input_xr = input_xr.to_array() + + # stack across pixel dimensions, handling timeseries if necessary + if "time" in input_xr.dims: + stacked = input_xr.stack(z=["x", "y", "time"]) + else: + stacked = input_xr.stack(z=["x", "y"]) + + # finding 'bands' dimensions in each pixel - these will not be + # flattened as their context is important for sklearn + pxdims = [] + for dim in stacked.dims: + if dim != "z": + pxdims.append(dim) + + # mask NaNs - we mask pixels with NaNs in *any* band, because + # sklearn cannot accept NaNs as input + mask = np.isnan(stacked) + if len(pxdims) != 0: + mask = mask.any(dim=pxdims) + + # turn the mask into a numpy array (boolean indexing with xarrays + # acts weird) + mask = mask.data + + # the dimension we are masking along ('z') needs to be the first + # dimension in the underlying np array for the boolean indexing to work + stacked = stacked.transpose("z", *pxdims) + input_np = stacked.data[~mask] + + return input_np + + +def sklearn_unflatten(output_np, input_xr): + """ + Reshape a numpy array with no 'missing' elements (NaNs) and + 'flattened' spatiotemporal structure into a DataArray matching the + spatiotemporal structure of the DataArray + + This enables an sklearn model's prediction to be remapped to the + correct pixels in the input DataArray or Dataset. + + Last modified: September 2019 + + Parameters + ---------- + output_np : numpy.array + The first dimension's length should correspond to the number of + valid (non-NaN) pixels in input_xr. + input_xr : xarray.DataArray or xarray.Dataset + Must have dimensions 'x' and 'y', may have dimension 'time'. + Dimensions other than 'x', 'y' and 'time' are unaffected by the + flattening. + + Returns + ---------- + output_xr : xarray.DataArray + An xarray.DataArray with the same dimensions 'x', 'y' and 'time' + as input_xr, and the same valid (non-NaN) pixels. These pixels + are set to match the data in output_np. + + """ + + # the output of a sklearn model prediction should just be a numpy array + # with size matching x*y*time for the input DataArray/Dataset. + + # cast input Datasets to DataArray + if isinstance(input_xr, xr.Dataset): + input_xr = input_xr.to_array() + + # generate the same mask we used to create the input to the sklearn model + if "time" in input_xr.dims: + stacked = input_xr.stack(z=["x", "y", "time"]) + else: + stacked = input_xr.stack(z=["x", "y"]) + + pxdims = [] + for dim in stacked.dims: + if dim != "z": + pxdims.append(dim) + + mask = np.isnan(stacked) + if len(pxdims) != 0: + mask = mask.any(dim=pxdims) + + # handle multivariable output + output_px_shape = () + if len(output_np.shape[1:]): + output_px_shape = output_np.shape[1:] + + # use the mask to put the data in all the right places + output_ma = np.ma.empty((len(stacked.z), *output_px_shape)) + output_ma[~mask] = output_np + output_ma[mask] = np.ma.masked + + # set the stacked coordinate to match the input + output_xr = xr.DataArray( + output_ma, + coords={"z": stacked["z"]}, + dims=["z", *["output_dim_" + str(idx) for idx in range(len(output_px_shape))]], + ) + + output_xr = output_xr.unstack() + + return output_xr + + +def fit_xr(model, input_xr): + """ + Utilise our wrappers to fit a vanilla sklearn model. + + Last modified: September 2019 + + Parameters + ---------- + model : scikit-learn model or compatible object + Must have a fit() method that takes numpy arrays. + input_xr : xarray.DataArray or xarray.Dataset. + Must have dimensions 'x' and 'y', may have dimension 'time'. + + Returns + ---------- + model : a scikit-learn model which has been fitted to the data in + the pixels of input_xr. + + """ + + model = model.fit(sklearn_flatten(input_xr)) + return model + + +def predict_xr( + model, + input_xr, + chunk_size=None, + persist=False, + proba=False, + clean=True, + return_input=False, +): + """ + Using dask-ml ParallelPostfit(), runs the parallel + predict and predict_proba methods of sklearn + estimators. Useful for running predictions + on a larger-than-RAM datasets. + + Last modified: September 2020 + + Parameters + ---------- + model : scikit-learn model or compatible object + Must have a .predict() method that takes numpy arrays. + input_xr : xarray.DataArray or xarray.Dataset. + Must have dimensions 'x' and 'y' + chunk_size : int + The dask chunk size to use on the flattened array. If this + is left as None, then the chunks size is inferred from the + .chunks method on the `input_xr` + persist : bool + If True, and proba=True, then 'input_xr' data will be + loaded into distributed memory. This will ensure data + is not loaded twice for the prediction of probabilities, + but this will only work if the data is not larger than + distributed RAM. + proba : bool + If True, predict probabilities + clean : bool + If True, remove Infs and NaNs from input and output arrays + return_input : bool + If True, then the data variables in the 'input_xr' dataset will + be appended to the output xarray dataset. + + Returns + ---------- + output_xr : xarray.Dataset + An xarray.Dataset containing the prediction output from model. + if proba=True then dataset will also contain probabilites, and + if return_input=True then dataset will have the input feature layers. + Has the same spatiotemporal structure as input_xr. + + """ + # if input_xr isn't dask, coerce it + dask = True + if not bool(input_xr.chunks): + dask = False + input_xr = input_xr.chunk({"x": len(input_xr.x), "y": len(input_xr.y)}) + + # set chunk size if not supplied + if chunk_size is None: + chunk_size = int(input_xr.chunks["x"][0]) * int(input_xr.chunks["y"][0]) + + def _predict_func(model, input_xr, persist, proba, clean, return_input): + x, y, crs = input_xr.x, input_xr.y, input_xr.geobox.crs + + input_data = [] + + for var_name in input_xr.data_vars: + input_data.append(input_xr[var_name]) + + input_data_flattened = [] + + for arr in input_data: + data = arr.data.flatten().rechunk(chunk_size) + input_data_flattened.append(data) + + # reshape for prediction + input_data_flattened = da.array(input_data_flattened).transpose() + + if clean == True: + input_data_flattened = da.where( + da.isfinite(input_data_flattened), input_data_flattened, 0 + ) + + if (proba == True) & (persist == True): + # persisting data so we don't require loading all the data twice + input_data_flattened = input_data_flattened.persist() + + # apply the classification + print("predicting...") + out_class = model.predict(input_data_flattened) + + # Mask out NaN or Inf values in results + if clean == True: + out_class = da.where(da.isfinite(out_class), out_class, 0) + + # Reshape when writing out + out_class = out_class.reshape(len(y), len(x)) + + # stack back into xarray + output_xr = xr.DataArray(out_class, coords={"x": x, "y": y}, dims=["y", "x"]) + + output_xr = output_xr.to_dataset(name="Predictions") + + if proba == True: + print(" probabilities...") + out_proba = model.predict_proba(input_data_flattened) + + # convert to % + out_proba = da.max(out_proba, axis=1) * 100.0 + + if clean == True: + out_proba = da.where(da.isfinite(out_proba), out_proba, 0) + + out_proba = out_proba.reshape(len(y), len(x)) + + out_proba = xr.DataArray( + out_proba, coords={"x": x, "y": y}, dims=["y", "x"] + ) + output_xr["Probabilities"] = out_proba + + if return_input == True: + print(" input features...") + # unflatten the input_data_flattened array and append + # to the output_xr containin the predictions + arr = input_xr.to_array() + stacked = arr.stack(z=["y", "x"]) + + # handle multivariable output + output_px_shape = () + if len(input_data_flattened.shape[1:]): + output_px_shape = input_data_flattened.shape[1:] + + output_features = input_data_flattened.reshape( + (len(stacked.z), *output_px_shape) + ) + + # set the stacked coordinate to match the input + output_features = xr.DataArray( + output_features, + coords={"z": stacked["z"]}, + dims=[ + "z", + *["output_dim_" + str(idx) for idx in range(len(output_px_shape))], + ], + ).unstack() + + # convert to dataset and rename arrays + output_features = output_features.to_dataset(dim="output_dim_0") + data_vars = list(input_xr.data_vars) + output_features = output_features.rename( + {i: j for i, j in zip(output_features.data_vars, data_vars)} + ) + + # merge with predictions + output_xr = xr.merge([output_xr, output_features], compat="override") + + return assign_crs(output_xr, str(crs)) + + if dask == True: + # convert model to dask predict + model = ParallelPostFit(model) + with joblib.parallel_backend("dask", wait_for_workers_timeout=20): + output_xr = _predict_func( + model, input_xr, persist, proba, clean, return_input + ) + + else: + output_xr = _predict_func( + model, input_xr, persist, proba, clean, return_input + ).compute() + + return output_xr + + +class HiddenPrints: + """ + For concealing unwanted print statements called by other functions + """ + + def __enter__(self): + self._original_stdout = sys.stdout + sys.stdout = open(os.devnull, "w") + + def __exit__(self, exc_type, exc_val, exc_tb): + sys.stdout.close() + sys.stdout = self._original_stdout + + +def _get_training_data_for_shp( + gdf, + index, + row, + out_arrs, + out_vars, + dc_query, + return_coords, + feature_func=None, + field=None, + zonal_stats=None, +): + """ + This is the core function that is triggered by `collect_training_data`. + The `collect_training_data` function loops through geometries in a geopandas + geodataframe and runs the code within `_get_training_data_for_shp`. + Parameters are inherited from `collect_training_data`. + See that function for information on the other params not listed below. + + Parameters + ---------- + index, row : iterables inherited from geopandas object + out_arrs : list + An empty list into which the training data arrays are stored. + out_vars : list + An empty list into which the data varaible names are stored. + + + Returns + -------- + Two lists, a list of numpy.arrays containing classes and extracted data for + each pixel or polygon, and another containing the data variable names. + + """ + + # prevent function altering dictionary kwargs + dc_query = deepcopy(dc_query) + + # remove dask chunks if supplied as using + # mulitprocessing for parallization + if "dask_chunks" in dc_query.keys(): + dc_query.pop("dask_chunks", None) + + # set up query based on polygon + geom = geometry.Geometry(geom=gdf.iloc[index].geometry, crs=gdf.crs) + q = {"geopolygon": geom} + + # merge polygon query with user supplied query params + dc_query.update(q) + + # Use input feature function + data = feature_func(dc_query) + + # create polygon mask + mask = xr_rasterize(gdf.iloc[[index]], data) + data = data.where(mask) + + # Check that feature_func has removed time + if "time" in data.dims: + t = data.dims["time"] + if t > 1: + raise ValueError( + "After running the feature_func, the dataset still has " + + str(t) + + " time-steps, dataset must only have" + + " x and y dimensions." + ) + + if return_coords == True: + # turn coords into a variable in the ds + data["x_coord"] = data.x + 0 * data.y + data["y_coord"] = data.y + 0 * data.x + + # append ID measurement to dataset for tracking failures + band = [m for m in data.data_vars][0] + _id = xr.zeros_like(data[band]) + data["id"] = _id + data["id"] = data["id"] + gdf.iloc[index]["id"] + + # If no zonal stats were requested then extract all pixel values + if zonal_stats is None: + flat_train = sklearn_flatten(data) + flat_val = np.repeat(row[field], flat_train.shape[0]) + stacked = np.hstack((np.expand_dims(flat_val, axis=1), flat_train)) + + elif zonal_stats in ["mean", "median", "max", "min"]: + method_to_call = getattr(data, zonal_stats) + flat_train = method_to_call() + flat_train = flat_train.to_array() + stacked = np.hstack((row[field], flat_train)) + + else: + raise Exception( + zonal_stats + + " is not one of the supported" + + " reduce functions ('mean','median','max','min')" + ) + + out_arrs.append(stacked) + out_vars.append([field] + list(data.data_vars)) + + +def _get_training_data_parallel( + gdf, dc_query, ncpus, return_coords, feature_func=None, field=None, zonal_stats=None +): + """ + Function passing the '_get_training_data_for_shp' function + to a mulitprocessing.Pool. + Inherits variables from 'collect_training_data'. + + """ + # Check if dask-client is running + try: + zx = None + zx = dd.get_client() + except: + pass + + if zx is not None: + raise ValueError( + "You have a Dask Client running, which prevents \n" + "this function from multiprocessing. Close the client." + ) + + # instantiate lists that can be shared across processes + manager = mp.Manager() + results = manager.list() + column_names = manager.list() + + # progress bar + pbar = tqdm(total=len(gdf)) + + def update(*a): + pbar.update() + + with mp.Pool(ncpus) as pool: + for index, row in gdf.iterrows(): + pool.apply_async( + _get_training_data_for_shp, + [ + gdf, + index, + row, + results, + column_names, + dc_query, + return_coords, + feature_func, + field, + zonal_stats, + ], + callback=update, + ) + + pool.close() + pool.join() + pbar.close() + + return column_names, results + + +def collect_training_data( + gdf, + dc_query, + ncpus=1, + return_coords=False, + feature_func=None, + field=None, + zonal_stats=None, + clean=True, + fail_threshold=0.02, + fail_ratio=0.5, + max_retries=3, +): + """ + This function provides methods for gathering training data from the ODC over + geometries stored within a geopandas geodataframe. The function will return a + 'model_input' array containing stacked training data arrays with all NaNs & Infs removed. + In the instance where ncpus > 1, a parallel version of the function will be run + (functions are passed to a mp.Pool()). This function can conduct zonal statistics if + the supplied shapefile contains polygons. The 'feature_func' parameter defines what + features to produce. + + Parameters + ---------- + gdf : geopandas geodataframe + geometry data in the form of a geopandas geodataframe + dc_query : dictionary + Datacube query object, should not contain lat and long (x or y) + variables as these are supplied by the 'gdf' variable + ncpus : int + The number of cpus/processes over which to parallelize the gathering + of training data (only if ncpus is > 1). Use 'mp.cpu_count()' to determine the number of + cpus available on a machine. Defaults to 1. + return_coords : bool + If True, then the training data will contain two extra columns 'x_coord' and + 'y_coord' corresponding to the x,y coordinate of each sample. This variable can + be useful for handling spatial autocorrelation between samples later in the ML workflow. + feature_func : function + A function for generating feature layers that is applied to the data within + the bounds of the input geometry. The 'feature_func' must accept a 'dc_query' + object, and return a single xarray.Dataset or xarray.DataArray containing + 2D coordinates (i.e x and y, without a third dimension). + e.g.:: + + def feature_function(query): + dc = datacube.Datacube(app='feature_layers') + ds = dc.load(**query) + ds = ds.mean('time') + return ds + + field : str + Name of the column in the gdf that contains the class labels + zonal_stats : string, optional + An optional string giving the names of zonal statistics to calculate + for each polygon. Default is None (all pixel values are returned). Supported + values are 'mean', 'median', 'max', 'min'. + clean : bool + Whether or not to remove missing values in the training dataset. If True, + training labels with any NaNs or Infs in the feature layers will be dropped + from the dataset. + fail_threshold : float, default 0.02 + Silent read fails on S3 during mulitprocessing can result in some rows + of the returned data containing NaN values. + The'fail_threshold' fraction specifies a % of acceptable fails. + e.g. Setting 'fail_threshold' to 0.05 means if >5% of the samples in the training dataset + fail then those samples will be returned to the multiprocessing queue. Below this fraction + the function will accept the failures and return the results. + fail_ratio: float + A float between 0 and 1 that defines if a given training sample has failed. + Default is 0.5, which means if 50 % of the measurements in a given sample return null + values, and the number of total fails is more than the 'fail_threshold', the sample + will be passed to the retry queue. + max_retries: int, default 3 + Maximum number of times to retry collecting samples. This number is invoked + if the 'fail_threshold' is not reached. + + Returns + -------- + Two objects are returned: + `columns_names`: a list of variable (feature) names + `model_input`: a numpy.array containing the data values for each feature extracted + + """ + + # check the dtype of the class field + if gdf[field].dtype != int: + raise ValueError( + 'The "field" column of the input vector must contain integer dtypes' + ) + + # set up some print statements + if feature_func is None: + raise ValueError( + "Please supply a feature layer function through the " + +"parameter 'feature_func'" + ) + + if zonal_stats is not None: + print("Taking zonal statistic: " + zonal_stats) + + # add unique id to gdf to help with indexing failed rows + # during multiprocessing + # if zonal_stats is not None: + gdf["id"] = range(0, len(gdf)) + + if ncpus == 1: + # progress indicator + print("Collecting training data in serial mode") + i = 0 + + # list to store results + results = [] + column_names = [] + + # loop through polys and extract training data + for index, row in gdf.iterrows(): + print(" Feature {:04}/{:04}\r".format(i + 1, len(gdf)), end="") + + _get_training_data_for_shp( + gdf, + index, + row, + results, + column_names, + dc_query, + return_coords, + feature_func, + field, + zonal_stats, + ) + i += 1 + + else: + print("Collecting training data in parallel mode") + column_names, results = _get_training_data_parallel( + gdf=gdf, + dc_query=dc_query, + ncpus=ncpus, + return_coords=return_coords, + feature_func=feature_func, + field=field, + zonal_stats=zonal_stats, + ) + + # column names are appended during each iteration + # but they are identical, grab only the first instance + column_names = column_names[0] + + # Stack the extracted training data for each feature into a single array + model_input = np.vstack(results) + + # this code block below iteratively retries failed rows + # up to max_retries or until fail_threshold is + # reached - whichever occurs first + if ncpus > 1: + i = 1 + while i <= max_retries: + # Find % of fails (null values) in data. Use Pandas for simplicity + df = pd.DataFrame(data=model_input[:, 0:-1], index=model_input[:, -1]) + # how many nan values per id? + num_nans = df.isnull().sum(axis=1) + num_nans = num_nans.groupby(num_nans.index).sum() + # how many valid values per id? + num_valid = df.notnull().sum(axis=1) + num_valid = num_valid.groupby(num_valid.index).sum() + # find fail rate + perc_fail = num_nans / (num_nans + num_valid) + fail_ids = perc_fail[perc_fail > fail_ratio] + fail_rate = len(fail_ids) / len(gdf) + + print( + "Percentage of possible fails after run " + + str(i) + + " = " + + str(round(fail_rate * 100, 2)) + + " %" + ) + + if fail_rate > fail_threshold: + print("Recollecting samples that failed") + + fail_ids = list(fail_ids.index) + # keep only the ids in model_input object that didn't fail + model_input = model_input[~np.isin(model_input[:, -1], fail_ids)] + + # index out the fail_ids from the original gdf + gdf_rerun = gdf.loc[gdf["id"].isin(fail_ids)] + gdf_rerun = gdf_rerun.reset_index(drop=True) + + time.sleep(5) # sleep for 5s to rest api + + # recollect failed rows + column_names_again, results_again = _get_training_data_parallel( + gdf=gdf_rerun, + dc_query=dc_query, + ncpus=ncpus, + return_coords=return_coords, + feature_func=feature_func, + field=field, + zonal_stats=zonal_stats, + ) + + # Stack the extracted training data for each feature into a single array + model_input_again = np.vstack(results_again) + + # merge results of the re-run with original run + model_input = np.vstack((model_input, model_input_again)) + + i += 1 + + else: + break + + # ----------------------------------------------- + + # remove id column + idx_var = column_names[0:-1] + model_col_indices = [column_names.index(var_name) for var_name in idx_var] + model_input = model_input[:, model_col_indices] + + if clean == True: + num = np.count_nonzero(np.isnan(model_input).any(axis=1)) + model_input = model_input[~np.isnan(model_input).any(axis=1)] + model_input = model_input[~np.isinf(model_input).any(axis=1)] + print("Removed " + str(num) + " rows wth NaNs &/or Infs") + print("Output shape: ", model_input.shape) + + else: + print("Returning data without cleaning") + print("Output shape: ", model_input.shape) + + return column_names[0:-1], model_input + + +class KMeans_tree(ClusterMixin): + """ + A hierarchical KMeans unsupervised clustering model. This class is + a clustering model, so it inherits scikit-learn's ClusterMixin + base class. + + Parameters + ---------- + n_levels : integer, default 2 + number of levels in the tree of clustering models. + n_clusters : integer, default 3 + Number of clusters in each of the constituent KMeans models in + the tree. + **kwargs : optional + Other keyword arguments to be passed directly to the KMeans + initialiser. + + """ + + def __init__(self, n_levels=2, n_clusters=3, **kwargs): + + assert n_levels >= 1 + + self.base_model = KMeans(n_clusters=3, **kwargs) + self.n_levels = n_levels + self.n_clusters = n_clusters + # make child models + if n_levels > 1: + self.branches = [ + KMeans_tree(n_levels=n_levels - 1, n_clusters=n_clusters, **kwargs) + for _ in range(n_clusters) + ] + + def fit(self, X, y=None, sample_weight=None): + """ + Fit the tree of KMeans models. All parameters mimic those + of KMeans.fit(). + + Parameters + ---------- + X : array-like or sparse matrix, shape=(n_samples, n_features) + Training instances to cluster. It must be noted that the + data will be converted to C ordering, which will cause a + memory copy if the given data is not C-contiguous. + y : Ignored + not used, present here for API consistency by convention. + sample_weight : array-like, shape (n_samples,), optional + The weights for each observation in X. If None, all + observations are assigned equal weight (default: None) + """ + + self.labels_ = self.base_model.fit(X, sample_weight=sample_weight).labels_ + + if self.n_levels > 1: + labels_old = np.copy(self.labels_) + # make room to add the sub-cluster labels + self.labels_ *= (self.n_clusters) ** (self.n_levels - 1) + + for clu in range(self.n_clusters): + # fit child models on their corresponding partition of the training set + self.branches[clu].fit( + X[labels_old == clu], + sample_weight=( + sample_weight[labels_old == clu] + if sample_weight is not None + else None + ), + ) + self.labels_[labels_old == clu] += self.branches[clu].labels_ + + return self + + def predict(self, X, sample_weight=None): + """ + Send X through the KMeans tree and predict the resultant + cluster. Compatible with KMeans.predict(). + + Parameters + ---------- + X : {array-like, sparse matrix}, shape = [n_samples, n_features] + New data to predict. + sample_weight : array-like, shape (n_samples,), optional + The weights for each observation in X. If None, all + observations are assigned equal weight (default: None) + + Returns + ------- + labels : array, shape [n_samples,] + Index of the cluster each sample belongs to. + """ + + result = self.base_model.predict(X, sample_weight=sample_weight) + + if self.n_levels > 1: + rescpy = np.copy(result) + + # make room to add the sub-cluster labels + result *= (self.n_clusters) ** (self.n_levels - 1) + + for clu in range(self.n_clusters): + result[rescpy == clu] += self.branches[clu].predict( + X[rescpy == clu], + sample_weight=( + sample_weight[rescpy == clu] + if sample_weight is not None + else None + ), + ) + + return result + + +def spatial_clusters( + coordinates, + method="Hierarchical", + max_distance=None, + n_groups=None, + verbose=False, + **kwargs +): + """ + Create spatial groups on coorindate data using either KMeans clustering + or a Gaussian Mixture model + + Last modified: September 2020 + + Parameters + ---------- + n_groups : int + The number of groups to create. This is passed as ``n_clusters=n_groups`` + for the KMeans algo, and ``n_components=n_groups`` for the GMM. If using + method=``'Hierarchical'`` then this parameter is ignored. + coordinates : np.array + A numpy array of coordinate values e.g.:: + + np.array([[3337270., 262400.], + [3441390., -273060.], ...]) + + method : str + Which algorithm to use to seperate data points. + Either ``'KMeans'``, ``'GMM'``, or ``'Hierarchical'``. + If using ``'Hierarchical'`` then must set max_distance. + max_distance : int + If method is set to ``'Hierarchical'`` then maximum distance describes the + maximum euclidean distances between all observations in a cluster. 'n_groups' + is ignored in this case. + **kwargs : optional, + Additional keyword arguments to pass to ``sklearn.cluster.Kmeans`` or + ``sklearn.mixture.GuassianMixture`` depending on the 'method' argument. + Returns + ------- + labels : array, shape [n_samples,] + Index of the cluster each sample belongs to. + """ + if method not in ["Hierarchical", "KMeans", "GMM"]: + raise ValueError("method must be one of: 'Hierarchical','KMeans' or 'GMM'") + + if (method in ["GMM", "KMeans"]) & (n_groups is None): + raise ValueError( + "The 'GMM' and 'KMeans' methods requires explicitly setting 'n_groups'" + ) + + if (method == "Hierarchical") & (max_distance is None): + raise ValueError("The 'Hierarchical' method requires setting max_distance") + + if method == "Hierarchical": + cluster_label = AgglomerativeClustering( + n_clusters=None, + linkage="complete", + distance_threshold=max_distance, + **kwargs + ).fit_predict(coordinates) + + if method == "KMeans": + cluster_label = KMeans(n_clusters=n_groups, **kwargs).fit_predict(coordinates) + + if method == "GMM": + cluster_label = GaussianMixture(n_components=n_groups, **kwargs).fit_predict( + coordinates + ) + if verbose: + print("n clusters = " + str(len(np.unique(cluster_label)))) + + return cluster_label + + +def SKCV( + coordinates, + n_splits, + cluster_method, + kfold_method, + test_size, + balance, + n_groups=None, + max_distance=None, + train_size=None, + random_state=None, + **kwargs +): + """ + Generate spatial k-fold cross validation indices using coordinate data. + + This function wraps the ``SpatialShuffleSplit`` and ``SpatialKFold`` classes. + These classes ingest coordinate data in the form of an + ``np.array([[eastings, northings]])`` and assign samples to a spatial cluster + using either a KMeans, Gaussain Mixture, or Agglomerative Clustering algorithm. + This cross-validator is preferred over other sklearn.model_selection methods + for spatial data to avoid overestimating cross-validation scores. + This can happen because of the inherent spatial autocorrelation that is usually + associated with this type of data. + + Last modified: Dec 2020 + + Parameters + ---------- + coordinates : np.array + A numpy array of coordinate values e.g.:: + + np.array([[3337270., 262400.], + [3441390., -273060.], ...]) + + n_splits : int + The number of test-train cross validation splits to generate. + cluster_method : str + Which algorithm to use to separate data points. Either ``'KMeans'``, + ``'GMM'``, or ``'Hierarchical'`` + kfold_method : str + One of either ``'SpatialShuffleSplit'`` or ``'SpatialKFold'``. See the docs + under class:_SpatialShuffleSplit and class:_SpatialKFold for more + information on these options. + test_size : float, int, None + If float, should be between 0.0 and 1.0 and represent the proportion + of the dataset to include in the test split. If int, represents the + absolute number of test samples. If None, the value is set to the + complement of the train size. If ``train_size`` is also None, it will + be set to 0.15. + balance : int or bool + if setting kfold_method to ``'SpatialShuffleSplit'``: int + The number of splits generated per iteration to try to balance the + amount of data in each set so that *test_size* and *train_size* are + respected. If 1, then no extra splits are generated (essentially + disabling the balacing). Must be >= 1. + + if setting kfold_method to ``'SpatialKFold'``: bool + Whether or not to split clusters into fold with approximately equal + number of data points. If False, each fold will have the same number of + clusters (which can have different number of data points in them). + + n_groups : int + The number of groups to create. This is passed as 'n_clusters=n_groups' + for the KMeans algo, and 'n_components=n_groups' for the GMM. If using + cluster_method='Hierarchical' then this parameter is ignored. + max_distance : int + If method is set to 'hierarchical' then maximum distance describes the + maximum euclidean distances between all observations in a cluster. 'n_groups' + is ignored in this case. + train_size : float, int, or None + If float, should be between 0.0 and 1.0 and represent the + proportion of the dataset to include in the train split. If + int, represents the absolute number of train samples. If None, + the value is automatically set to the complement of the test size. + random_state : int, RandomState instance or None, optional (default=None) + If int, random_state is the seed used by the random number generator; + If RandomState instance, random_state is the random number generator; + If None, the random number generator is the RandomState instance used + by ``np.random``. + **kwargs : optional, + Additional keyword arguments to pass to sklearn.cluster.Kmeans or + sklearn.mixture.GuassianMixture depending on the cluster_method argument. + Returns + -------- + generator object _BaseSpatialCrossValidator.split + + """ + # intiate a method + if kfold_method == "SpatialShuffleSplit": + splitter = _SpatialShuffleSplit( + n_groups=n_groups, + method=cluster_method, + coordinates=coordinates, + max_distance=max_distance, + test_size=test_size, + train_size=train_size, + n_splits=n_splits, + random_state=random_state, + balance=balance, + **kwargs + ) + + if kfold_method == "SpatialKFold": + splitter = _SpatialKFold( + n_groups=n_groups, + coordinates=coordinates, + max_distance=max_distance, + method=cluster_method, + test_size=test_size, + n_splits=n_splits, + random_state=random_state, + balance=balance, + **kwargs + ) + + return splitter + + +def spatial_train_test_split( + X, + y, + coordinates, + cluster_method, + kfold_method, + balance, + test_size=None, + n_splits=None, + n_groups=None, + max_distance=None, + train_size=None, + random_state=None, + **kwargs +): + """ + Split arrays into random train and test subsets. Similar to + `sklearn.model_selection.train_test_split` but instead works on + spatial coordinate data. Coordinate data is grouped according + to either a KMeans, Gaussain Mixture, or Agglomerative Clustering algorthim. + Grouping by spatial clusters is preferred over plain random splits for + spatial data to avoid overestimating validation scores due to spatial + autocorrelation. + + Parameters + ---------- + X : np.array + Training data features + y : np.array + Training data labels + coordinates : np.array + A numpy array of coordinate values e.g.:: + + np.array([[3337270., 262400.], + [3441390., -273060.], ...]) + + cluster_method : str + Which algorithm to use to seperate data points. + Either ``'KMeans'``, ``'GMM'``, or ``'Hierarchical'`` + kfold_method : str + One of either ``'SpatialShuffleSplit'`` or ``'SpatialKFold'``. + See the docs under class:_SpatialShuffleSplit and + class: _SpatialKFold for more information on these options. + balance : int or bool + if setting kfold_method to ''`SpatialShuffleSplit`'': int + The number of splits generated per iteration to try to balance the + amount of data in each set so that *test_size* and *train_size* are + respected. If 1, then no extra splits are generated (essentially + disabling the balacing). Must be >= 1. + + if setting kfold_method to ''`SpatialKFold`'': bool + Whether or not to split clusters into fold with approximately equal + number of data points. If False, each fold will have the same number of + clusters (which can have different number of data points in them). + + test_size : float, int, None + If float, should be between 0.0 and 1.0 and represent the proportion + of the dataset to include in the test split. If int, represents the + absolute number of test samples. If None, the value is set to the + complement of the train size. If ``train_size`` is also None, it will + be set to 0.15. + n_splits : int + This parameter is invoked for the 'SpatialKFold' folding method, use this + number to satisfy the train-test size ratio desired, as the 'test_size' + parameter for the KFold method often fails to get the ratio right. + n_groups : int + The number of groups to create. This is passed as 'n_clusters=n_groups' + for the KMeans algo, and 'n_components=n_groups' for the GMM. If using + cluster_method='Hierarchical' then this parameter is ignored. + max_distance : int + If method is set to 'hierarchical' then maximum distance describes the + maximum euclidean distances between all observations in a cluster. 'n_groups' + is ignored in this case. + train_size : float, int, or None + If float, should be between 0.0 and 1.0 and represent the + proportion of the dataset to include in the train split. If + int, represents the absolute number of train samples. If None, + the value is automatically set to the complement of the test size. + random_state : int, + RandomState instance or None, optional + If int, random_state is the seed used by the random number generator; + If RandomState instance, random_state is the random number generator; + If None, the random number generator is the RandomState instance used + by `np.random`. + **kwargs : optional, + Additional keyword arguments to pass to sklearn.cluster.Kmeans or + sklearn.mixture.GuassianMixture depending on the cluster_method argument. + + Returns + ------- + Tuple : + Contains four arrays in the following order: + X_train, X_test, y_train, y_test + + """ + + if kfold_method == "SpatialShuffleSplit": + splitter = _SpatialShuffleSplit( + n_groups=n_groups, + method=cluster_method, + coordinates=coordinates, + max_distance=max_distance, + test_size=test_size, + train_size=train_size, + n_splits=1 if n_splits is None else n_splits, + random_state=random_state, + balance=balance, + **kwargs + ) + + if kfold_method == "SpatialKFold": + if n_splits is None: + raise ValueError( + "n_splits parameter requires an integer value, eg. 'n_splits=5'" + ) + if (test_size is not None) or (train_size is not None): + warnings.warn( + "With the 'SpatialKFold' method, controlling the test/train ratio " + "is better achieved using the 'n_splits' parameter" + ) + + splitter = _SpatialKFold( + n_groups=n_groups, + coordinates=coordinates, + max_distance=max_distance, + method=cluster_method, + n_splits=n_splits, + random_state=random_state, + balance=balance, + **kwargs + ) + + lst = [] + for train, test in splitter.split(coordinates): + X_tr, X_tt = X[train, :], X[test, :] + y_tr, y_tt = y[train], y[test] + lst.extend([X_tr, X_tt, y_tr, y_tt]) + + return (lst[0], lst[1], lst[2], lst[3]) + + +def _partition_by_sum(array, parts): + """ + Partition an array into parts of approximately equal sum. + Does not change the order of the array elements. + Produces the partition indices on the array. Use :func:`numpy.split` to + divide the array along these indices. + Parameters + ---------- + array : array or array-like + The 1D array that will be partitioned. The array will be raveled before + computations. + parts : int + Number of parts to split the array. Can be at most the number of + elements in the array. + Returns + ------- + indices : array + The indices in which the array should be split. + Notes + ----- + Solution from https://stackoverflow.com/a/54024280 + """ + array = np.atleast_1d(array).ravel() + if parts > array.size: + raise ValueError( + "Cannot partition an array of size {} into {} parts of equal sum.".format( + array.size, parts + ) + ) + cumulative_sum = array.cumsum() + # Ideally, we want each part to have the same number of points (total / + # parts). + ideal_sum = cumulative_sum[-1] // parts + # If the parts are ideal, the cumulative sum of each part will be this + ideal_cumsum = np.arange(1, parts) * ideal_sum + indices = np.searchsorted(cumulative_sum, ideal_cumsum, side="right") + # Check for repeated split points, which indicates that there is no way to + # split the array. + if np.unique(indices).size != indices.size: + raise ValueError( + "Could not find partition points to split the array into {} parts " + "of equal sum.".format(parts) + ) + return indices + + +class _BaseSpatialCrossValidator(BaseCrossValidator, metaclass=ABCMeta): + """ + Base class for spatial cross-validators. + Parameters + ---------- + n_groups : int + The number of groups to create. This is passed as 'n_clusters=n_groups' + for the KMeans algo, and 'n_components=n_groups' for the GMM. + coordinates : np.array + A numpy array of coordinate values e.g. + np.array([[3337270., 262400.], + [3441390., -273060.], ..., + method : str + Which algorithm to use to seperate data points. Either 'KMeans' or 'GMM' + n_splits : int + Number of splitting iterations. + """ + + def __init__( + self, + n_groups=None, + coordinates=None, + method=None, + max_distance=None, + n_splits=None, + ): + + self.n_groups = n_groups + self.coordinates = coordinates + self.method = method + self.max_distance = max_distance + self.n_splits = n_splits + + def split(self, X, y=None, groups=None): + """ + Generate indices to split data into training and test set. + Parameters + ---------- + X : array-like, shape (n_samples, 2) + Columns should be the easting and northing coordinates of data + points, respectively. + y : array-like, shape (n_samples,) + The target variable for supervised learning problems. Always + ignored. + groups : array-like, with shape (n_samples,), optional + Group labels for the samples used while splitting the dataset into + train/test set. Always ignored. + Yields + ------ + train : ndarray + The training set indices for that split. + test : ndarray + The testing set indices for that split. + """ + if X.shape[1] != 2: + raise ValueError( + "X (the coordinate data) must have exactly 2 columns ({} given).".format( + X.shape[1] + ) + ) + for train, test in super().split(X, y, groups): + yield train, test + + def get_n_splits(self, X=None, y=None, groups=None): + """ + Returns the number of splitting iterations in the cross-validator + Parameters + ---------- + X : object + Always ignored, exists for compatibility. + y : object + Always ignored, exists for compatibility. + groups : object + Always ignored, exists for compatibility. + Returns + ------- + n_splits : int + Returns the number of splitting iterations in the cross-validator. + """ + return self.n_splits + + @abstractmethod + def _iter_test_indices(self, X=None, y=None, groups=None): + """ + Generates integer indices corresponding to test sets. + MUST BE IMPLEMENTED BY DERIVED CLASSES. + Parameters + ---------- + X : array-like, shape (n_samples, 2) + Columns should be the easting and northing coordinates of data + points, respectively. + y : array-like, shape (n_samples,) + The target variable for supervised learning problems. Always + ignored. + groups : array-like, with shape (n_samples,), optional + Group labels for the samples used while splitting the dataset into + train/test set. Always ignored. + Yields + ------ + test : ndarray + The testing set indices for that split. + """ + + +class _SpatialShuffleSplit(_BaseSpatialCrossValidator): + """ + Random permutation of spatial cross-validator. + Yields indices to split data into training and test sets. Data are first + grouped into clusters using either a KMeans or GMM algorithm + and are then split into testing and training sets randomly. + The proportion of clusters assigned to each set is controlled by *test_size* + and/or *train_size*. However, the total amount of actual data points in + each set could be different from these values since clusters can have + a different number of data points inside them. To guarantee that the + proportion of actual data is as close as possible to the proportion of + clusters, this cross-validator generates an extra number of splits and + selects the one with proportion of data points in each set closer to the + desired amount. The number of balance splits per + iteration is controlled by the *balance* argument. + This cross-validator is preferred over `sklearn.model_selection.ShuffleSplit` + for spatial data to avoid overestimating cross-validation scores. + This can happen because of the inherent spatial autocorrelation. + Parameters + ---------- + n_groups : int + The number of groups to create. This is passed as 'n_clusters=n_groups' + for the KMeans algo, and 'n_components=n_groups' for the GMM. If using + cluster_method='Hierarchical' then this parameter is ignored. + coordinates : np.array + A numpy array of coordinate values e.g. + np.array([[3337270., 262400.], + [3441390., -273060.], ...]) + cluster_method : str + Which algorithm to use to seperate data points. Either 'KMeans', 'GMM', or + 'Hierarchical' + max_distance : int + If method is set to 'hierarchical' then maximum distance describes the + maximum euclidean distances between all observations in a cluster. 'n_groups' + is ignored in this case. + n_splits : int, + Number of re-shuffling & splitting iterations. + test_size : float, int, None + If float, should be between 0.0 and 1.0 and represent the proportion + of the dataset to include in the test split. If int, represents the + absolute number of test samples. If None, the value is set to the + complement of the train size. If ``train_size`` is also None, it will + be set to 0.1. + train_size : float, int, or None + If float, should be between 0.0 and 1.0 and represent the + proportion of the dataset to include in the train split. If + int, represents the absolute number of train samples. If None, + the value is automatically set to the complement of the test size. + random_state : int, RandomState instance or None, optional (default=None) + If int, random_state is the seed used by the random number generator; + If RandomState instance, random_state is the random number generator; + If None, the random number generator is the RandomState instance used + by `np.random`. + balance : int + The number of splits generated per iteration to try to balance the + amount of data in each set so that *test_size* and *train_size* are + respected. If 1, then no extra splits are generated (essentially + disabling the balacing). Must be >= 1. + **kwargs : optional, + Additional keyword arguments to pass to sklearn.cluster.Kmeans or + sklearn.mixture.GuassianMixture depending on the cluster_method argument. + Returns + -------- + generator + containing indices to split data into training and test sets + """ + + def __init__( + self, + n_groups=None, + coordinates=None, + method="Heirachical", + max_distance=None, + n_splits=None, + test_size=0.15, + train_size=None, + random_state=None, + balance=10, + **kwargs + ): + super().__init__( + n_groups=n_groups, + coordinates=coordinates, + method=method, + max_distance=max_distance, + n_splits=n_splits, + **kwargs + ) + if balance < 1: + raise ValueError( + "The *balance* argument must be >= 1. To disable balance, use 1." + ) + self.test_size = test_size + self.train_size = train_size + self.random_state = random_state + self.balance = balance + self.kwargs = kwargs + + def _iter_test_indices(self, X=None, y=None, groups=None): + """ + Generates integer indices corresponding to test sets. + Runs several iterations until a split is found that yields clusters with + the right amount of data points in it. + Parameters + ---------- + X : array-like, shape (n_samples, 2) + Columns should be the easting and northing coordinates of data + points, respectively. + y : array-like, shape (n_samples,) + The target variable for supervised learning problems. Always + ignored. + groups : array-like, with shape (n_samples,), optional + Group labels for the samples used while splitting the dataset into + train/test set. Always ignored. + Yields + ------ + test : ndarray + The testing set indices for that split. + """ + labels = spatial_clusters( + n_groups=self.n_groups, + coordinates=self.coordinates, + method=self.method, + max_distance=self.max_distance, + **self.kwargs + ) + + cluster_ids = np.unique(labels) + # Generate many more splits so that we can pick and choose the ones + # that have the right balance of training and testing data. + shuffle = ShuffleSplit( + n_splits=self.n_splits * self.balance, + test_size=self.test_size, + train_size=self.train_size, + random_state=self.random_state, + ).split(cluster_ids) + + for _ in range(self.n_splits): + test_sets, balance = [], [] + for _ in range(self.balance): + # This is a false positive in pylint which is why the warning + # is disabled at the top of this file: + # https://github.com/PyCQA/pylint/issues/1830 + # pylint: disable=stop-iteration-return + train_clusters, test_clusters = next(shuffle) + # pylint: enable=stop-iteration-return + train_points = np.where(np.isin(labels, cluster_ids[train_clusters]))[0] + test_points = np.where(np.isin(labels, cluster_ids[test_clusters]))[0] + # The proportion of data points assigned to each group should + # be close the proportion of clusters assigned to each group. + balance.append( + abs( + train_points.size / test_points.size + - train_clusters.size / test_clusters.size + ) + ) + test_sets.append(test_points) + best = np.argmin(balance) + yield test_sets[best] + + +class _SpatialKFold(_BaseSpatialCrossValidator): + """ + Spatial K-Folds cross-validator. + Yields indices to split data into training and test sets. Data are first + grouped into clusters using either a KMeans or GMM algorithm + clusters. The clusters are then split into testing and training sets iteratively + along k folds of the data (k is given by *n_splits*). + By default, the clusters are split into folds in a way that makes each fold + have approximately the same number of data points. Sometimes this might not + be possible, which can happen if the number of splits is close to the + number of clusters. In these cases, each fold will have the same number of + clusters regardless of how many data points are in each cluster. This + behaviour can also be disabled by setting ``balance=False``. + This cross-validator is preferred over `sklearn.model_selection.KFold` for + spatial data to avoid overestimating cross-validation scores. This can happen + because of the inherent autocorrelation that is usually associated with + this type of data. + Parameters + ---------- + n_groups : int + The number of groups to create. This is passed as 'n_clusters=n_groups' + for the KMeans algo, and 'n_components=n_groups' for the GMM. If using + cluster_method='Hierarchical' then this parameter is ignored. + coordinates : np.array + A numpy array of coordinate values e.g. + np.array([[3337270., 262400.], + [3441390., -273060.], ...]) + cluster_method : str + Which algorithm to use to seperate data points. Either 'KMeans', 'GMM', or + 'Hierarchical' + max_distance : int + If method is set to 'hierarchical' then maximum distance describes the + maximum euclidean distances between all observations in a cluster. 'n_groups' + is ignored in this case. + n_splits : int + Number of folds. Must be at least 2. + shuffle : bool + Whether to shuffle the data before splitting into batches. + random_state : int, RandomState instance or None, optional (defasult=None) + If int, random_state is the seed used by the random number generator; + If RandomState instance, random_state is the random number generator; + If None, the random number generator is the RandomState instance used + by `np.random`. + balance : bool + Whether or not to split clusters into fold with approximately equal + number of data points. If False, each fold will have the same number of + clusters (which can have different number of data points in them). + **kwargs : optional, + Additional keyword arguments to pass to sklearn.cluster.Kmeans or + sklearn.mixture.GuassianMixture depending on the cluster_method argument. + """ + + def __init__( + self, + n_groups=None, + coordinates=None, + method="Heirachical", + max_distance=None, + n_splits=5, + test_size=0.15, + train_size=None, + shuffle=True, + random_state=None, + balance=True, + **kwargs + ): + super().__init__( + n_groups=n_groups, + coordinates=coordinates, + method=method, + max_distance=max_distance, + n_splits=n_splits, + **kwargs + ) + + if n_splits < 2: + raise ValueError( + "Number of splits must be >=2 for clusterKFold. Given {}.".format( + n_splits + ) + ) + self.test_size = test_size + self.shuffle = shuffle + self.random_state = random_state + self.balance = balance + self.kwargs = kwargs + + def _iter_test_indices(self, X=None, y=None, groups=None): + """ + Generates integer indices corresponding to test sets. + Parameters + ---------- + X : array-like, shape (n_samples, 2) + Columns should be the easting and northing coordinates of data + points, respectively. + y : array-like, shape (n_samples,) + The target variable for supervised learning problems. Always + ignored. + groups : array-like, with shape (n_samples,), optional + Group labels for the samples used while splitting the dataset into + train/test set. Always ignored. + Yields + ------ + test : ndarray + The testing set indices for that split. + """ + labels = spatial_clusters( + n_groups=self.n_groups, + coordinates=self.coordinates, + method=self.method, + max_distance=self.max_distance, + **self.kwargs + ) + + cluster_ids = np.unique(labels) + if self.n_splits > cluster_ids.size: + raise ValueError( + "Number of k-fold splits ({}) cannot be greater than the number of " + "clusters ({}). Either decrease n_splits or increase the number of " + "clusters.".format(self.n_splits, cluster_ids.size) + ) + if self.shuffle: + check_random_state(self.random_state).shuffle(cluster_ids) + if self.balance: + cluster_sizes = [np.isin(labels, i).sum() for i in cluster_ids] + try: + split_points = _partition_by_sum(cluster_sizes, parts=self.n_splits) + folds = np.split(np.arange(cluster_ids.size), split_points) + except ValueError: + warnings.warn( + "Could not balance folds to have approximately the same " + "number of data points. Dividing into folds with equal " + "number of clusters instead. Decreasing n_splits or increasing " + "the number of clusters may help.", + UserWarning, + ) + folds = [i for _, i in KFold(n_splits=self.n_splits).split(cluster_ids)] + else: + folds = [i for _, i in KFold(n_splits=self.n_splits).split(cluster_ids)] + for test_clusters in folds: + test_points = np.where(np.isin(labels, cluster_ids[test_clusters]))[0] + yield test_points diff --git a/deafrica_tools/coastal.py b/deafrica_tools/coastal.py new file mode 100644 index 0000000..c76d3db --- /dev/null +++ b/deafrica_tools/coastal.py @@ -0,0 +1,1123 @@ +""" +Coastal analyses on Digital Earth Africa data. +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import requests +import numpy as np +import xarray as xr +import pandas as pd +import geopandas as gpd +import matplotlib.pyplot as plt +from scipy import stats +from otps import TimePoint +from otps import predict_tide +from shapely.geometry import box +from datacube.utils.geometry import CRS +from owslib.wfs import WebFeatureService + +from deafrica_tools.datahandling import parallel_apply + + +# Fix converters for tidal plot +from pandas.plotting import register_matplotlib_converters +register_matplotlib_converters() + + +# URL for the DE Africa Coastlines data on Geoserver. +WFS_ADDRESS = "https://geoserver.digitalearth.africa/geoserver/wfs" + +def model_tides( + x, + y, + time, + model="FES2014", + directory="/var/share/tide_models", + epsg=4326, + method="bilinear", + extrapolate=True, + cutoff=10.0, +): + """ + Compute tides at points and times using tidal harmonics. + If multiple x, y points are provided, tides will be + computed for all timesteps at each point. + + This function supports any tidal model supported by + `pyTMD`, including the FES2014 Finite Element Solution + tide model, and the TPXO8-atlas and TPXO9-atlas-v5 + TOPEX/POSEIDON global tide models. + + This function requires access to tide model data files + to work. These should be placed in a folder with + subfolders matching the formats specified by `pyTMD`: + https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories + + For FES2014 (https://www.aviso.altimetry.fr/es/data/products/auxiliary-products/global-tide-fes/description-fes2014.html): + - {directory}/fes2014/ocean_tide/ + {directory}/fes2014/load_tide/ + + For TPXO8-atlas (https://www.tpxo.net/tpxo-products-and-registration): + - {directory}/tpxo8_atlas/ + + For TPXO9-atlas-v5 (https://www.tpxo.net/tpxo-products-and-registration): + - {directory}/TPXO9_atlas_v5/ + + This function is a minor modification of the `pyTMD` + package's `compute_tide_corrections` function, adapted + to process multiple timesteps for multiple input point + locations. For more info: + https://pytmd.readthedocs.io/en/stable/user_guide/compute_tide_corrections.html + + Parameters: + ----------- + x, y : float or list of floats + One or more x and y coordinates used to define + the location at which to model tides. By default these + coordinates should be lat/lon; use `epsg` if they + are in a custom coordinate reference system. + time : A datetime array or pandas.DatetimeIndex + An array containing 'datetime64[ns]' values or a + 'pandas.DatetimeIndex' providing the times at which to + model tides in UTC time. + model : string + The tide model used to model tides. Options include: + - "FES2014" (only pre-configured option on DEA Sandbox) + - "TPXO8-atlas" + - "TPXO9-atlas-v5" + directory : string + The directory containing tide model data files. These + data files should be stored in sub-folders for each + model that match the structure provided by `pyTMD`: + https://pytmd.readthedocs.io/en/latest/getting_started/Getting-Started.html#directories + For example: + - {directory}/fes2014/ocean_tide/ + {directory}/fes2014/load_tide/ + - {directory}/tpxo8_atlas/ + - {directory}/TPXO9_atlas_v5/ + epsg : int + Input coordinate system for 'x' and 'y' coordinates. + Defaults to 4326 (WGS84). + method : string + Method used to interpolate tidal contsituents + from model files. Options include: + - bilinear: quick bilinear interpolation + - spline: scipy bivariate spline interpolation + - linear, nearest: scipy regular grid interpolations + extrapolate : bool + Whether to extrapolate tides for locations outside of + the tide modelling domain using nearest-neighbor + cutoff : int or float + Extrapolation cutoff in kilometers. Set to `np.inf` + to extrapolate for all points. + + Returns + ------- + A pandas.DataFrame containing tide heights for every + combination of time and point coordinates. + """ + + import os + import pyproj + import numpy as np + import pyTMD.time + import pyTMD.model + import pyTMD.utilities + from pyTMD.calc_delta_time import calc_delta_time + from pyTMD.infer_minor_corrections import infer_minor_corrections + from pyTMD.predict_tide_drift import predict_tide_drift + from pyTMD.read_tide_model import extract_tidal_constants + from pyTMD.read_netcdf_model import extract_netcdf_constants + from pyTMD.read_GOT_model import extract_GOT_constants + from pyTMD.read_FES_model import extract_FES_constants + + # Check that tide directory is accessible + try: + os.access(directory, os.F_OK) + except: + raise FileNotFoundError("Invalid tide directory") + + # Get parameters for tide model + model = pyTMD.model(directory, format="netcdf", compressed=False).elevation(model) + + # If time passed as a single Timestamp, convert to datetime64 + if isinstance(time, pd.Timestamp): + time = time.to_datetime64() + + # Handle numeric or array inputs + x = np.atleast_1d(x) + y = np.atleast_1d(y) + time = np.atleast_1d(time) + + # Determine point and time counts + assert len(x) == len(y), "x and y must be the same length" + n_points = len(x) + n_times = len(time) + + # Converting x,y from EPSG to latitude/longitude + try: + # EPSG projection code string or int + crs1 = pyproj.CRS.from_string("epsg:{0:d}".format(int(epsg))) + except (ValueError, pyproj.exceptions.CRSError): + # Projection SRS string + crs1 = pyproj.CRS.from_string(epsg) + + crs2 = pyproj.CRS.from_string("epsg:{0:d}".format(4326)) + transformer = pyproj.Transformer.from_crs(crs1, crs2, always_xy=True) + lon, lat = transformer.transform(x.flatten(), y.flatten()) + + # Assert delta time is an array and convert datetime + time = np.atleast_1d(time) + t = pyTMD.time.convert_datetime(time, epoch=(1992, 1, 1, 0, 0, 0)) / 86400.0 + + # Delta time (TT - UT1) file + delta_file = pyTMD.utilities.get_data_path(["data", "merged_deltat.data"]) + + # Read tidal constants and interpolate to grid points + if model.format in ("OTIS", "ATLAS"): + amp, ph, D, c = extract_tidal_constants( + lon, + lat, + model.grid_file, + model.model_file, + model.projection, + TYPE=model.type, + METHOD=method, + EXTRAPOLATE=extrapolate, + CUTOFF=cutoff, + GRID=model.format, + ) + deltat = np.zeros_like(t) + + elif model.format == "netcdf": + amp, ph, D, c = extract_netcdf_constants( + lon, + lat, + model.grid_file, + model.model_file, + TYPE=model.type, + METHOD=method, + EXTRAPOLATE=extrapolate, + CUTOFF=cutoff, + SCALE=model.scale, + GZIP=model.compressed, + ) + deltat = np.zeros_like(t) + + elif model.format == "GOT": + amp, ph, c = extract_GOT_constants( + lon, + lat, + model.model_file, + METHOD=method, + EXTRAPOLATE=extrapolate, + CUTOFF=cutoff, + SCALE=model.scale, + GZIP=model.compressed, + ) + + # Interpolate delta times from calendar dates to tide time + deltat = calc_delta_time(delta_file, t) + + elif model.format == "FES": + amp, ph = extract_FES_constants( + lon, + lat, + model.model_file, + TYPE=model.type, + VERSION=model.version, + METHOD=method, + EXTRAPOLATE=extrapolate, + CUTOFF=cutoff, + SCALE=model.scale, + GZIP=model.compressed, + ) + + # Available model constituents + c = model.constituents + + # Interpolate delta times from calendar dates to tide time + deltat = calc_delta_time(delta_file, t) + + # Calculate complex phase in radians for Euler's + cph = -1j * ph * np.pi / 180.0 + + # Calculate constituent oscillation + hc = amp * np.exp(cph) + + # Repeat constituents to length of time and number of input + # coords before passing to `predict_tide_drift` + t, hc, deltat = ( + np.tile(t, n_points), + hc.repeat(n_times, axis=0), + np.tile(deltat, n_points), + ) + + # Predict tidal elevations at time and infer minor corrections + npts = len(t) + tide = np.ma.zeros((npts), fill_value=np.nan) + tide.mask = np.any(hc.mask, axis=1) + + # Depending on pyTMD version (<=1.06 vs > 1.06), use different params + # TODO: Remove once Sandbox is updated to use pyTMD version 1.0.9 + try: + tide.data[:] = predict_tide_drift( + t, hc, c, deltat=deltat, corrections=model.format + ) + minor = infer_minor_corrections( + t, hc, c, deltat=deltat, corrections=model.format + ) + except: + tide.data[:] = predict_tide_drift( + t, hc, c, DELTAT=deltat, CORRECTIONS=model.format + ) + minor = infer_minor_corrections( + t, hc, c, DELTAT=deltat, CORRECTIONS=model.format + ) + tide.data[:] += minor.data[:] + + # Replace invalid values with fill value + tide.data[tide.mask] = tide.fill_value + + # Export data as a dataframe + return pd.DataFrame( + { + "time": np.tile(time, n_points), + "x": np.repeat(x, n_times), + "y": np.repeat(y, n_times), + "tide_m": tide, + } + ).set_index("time") + + +def pixel_tides( + ds, + times=None, + resample=True, + calculate_quantiles=None, + resolution=None, + buffer=None, + resample_method="bilinear", + **model_tides_kwargs, +): + """ + Obtain tide heights for each pixel in a dataset by modelling + tides into a low-resolution grid surrounding the dataset, + then (optionally) spatially resample this low-res data back + into the original higher resolution dataset extent and resolution. + + Parameters: + ----------- + ds : xarray.Dataset + A dataset whose geobox (`ds.odc.geobox`) will be used to define + the spatial extent of the low resolution tide modelling grid. + times : pandas.DatetimeIndex or list of pandas.Timestamps, optional + By default, the function will model tides using the times + contained in the `time` dimension of `ds`. Alternatively, this + param can be used to model tides for a custom set of times + instead. For example: + `times=pd.date_range(start="2000", end="2001", freq="5h")` + resample : bool, optional + Whether to resample low resolution tides back into `ds`'s original + higher resolution grid. Set this to `False` if you do not want + low resolution tides to be re-projected back to higher resolution. + calculate_quantiles : list or np.array, optional + Rather than returning all individual tides, low-resolution tides + can be first aggregated using a quantile calculation by passing in + a list or array of quantiles to compute. For example, this could + be used to calculate the min/max tide across all times: + `calculate_quantiles=[0.0, 1.0]`. + resolution: int, optional + The desired resolution of the low-resolution grid used for tide + modelling. The default None will create a 5000 m resolution grid + if `ds` has a projected CRS (i.e. metre units), or a 0.05 degree + resolution grid if `ds` has a geographic CRS (e.g. degree units). + Note: higher resolutions do not necessarily provide better + tide modelling performance, as results will be limited by the + resolution of the underlying global tide model (e.g. 1/16th + degree / ~5 km resolution grid for FES2014). + buffer : int, optional + The amount by which to buffer the higher resolution grid extent + when creating the new low resolution grid. This buffering is + important as it ensures that ensure pixel-based tides are seamless + across dataset boundaries. This buffer will eventually be clipped + away when the low-resolution data is re-projected back to the + resolution and extent of the higher resolution dataset. To + ensure that at least two pixels occur outside of the dataset + bounds, the default None applies a 12000 m buffer if `ds` has a + projected CRS (i.e. metre units), or a 0.12 degree buffer if + `ds` has a geographic CRS (e.g. degree units). + resample_method : string, optional + If resampling is requested (see `resample` above), use this + resampling method when converting from low resolution to high + resolution pixels. Defaults to "bilinear"; valid options include + "nearest", "cubic", "min", "max", "average" etc. + **model_tides_kwargs : + Optional parameters passed to the `dea_tools.coastal.model_tides` + function. Important parameters include "model" and "directory", + used to specify the tide model to use and the location of its files. + + Returns: + -------- + If `resample` is True: + + tides_lowres : xr.DataArray + A low resolution data array giving either tide heights every + timestep in `ds` (if `times` is None), tide heights at every + time in `times` (if `times` is not None), or tide height quantiles + for every quantile provided by `calculate_quantiles`. + + If `resample` is False: + + tides_highres, tides_lowres : tuple of xr.DataArrays + In addition to `tides_lowres` (see above), a high resolution + array of tide heights will be generated that matches the + exact spatial resolution and extent of `ds`. This will contain + either tide heights every timestep in `ds` (if `times` is None), + tide heights at every time in `times` (if `times` is not None), + or tide height quantiles for every quantile provided by + `calculate_quantiles`. + """ + + import odc.geo.xr + from odc.geo.geobox import GeoBox + + # First test if no time dimension and nothing passed to `times` + if ('time' not in ds.dims) & (times is None): + raise ValueError( + "`ds` does not contain a 'time' dimension. Times are required " + "for modelling tides: please pass in a set of custom tides " + "using the `times` parameter. For example: " + "`times=pd.date_range(start='2000', end='2001', freq='5h')`" + ) + + # If custom times are provided, convert them to a consistent + # pandas.DatatimeIndex format + if times is not None: + if isinstance(times, list): + time_coords = pd.DatetimeIndex(times) + elif isinstance(times, pd.Timestamp): + time_coords = pd.DatetimeIndex([times]) + else: + time_coords = times + + # Otherwise, use times from `ds` directly + else: + time_coords = ds.coords["time"] + + # Determine spatial dimensions + y_dim, x_dim = ds.odc.spatial_dims + + # Determine resolution and buffer, using different defaults for + # geographic (i.e. degrees) and projected (i.e. metres) CRSs: + crs_units = ds.odc.geobox.crs.units[0][0:6] + if ds.odc.geobox.crs.geographic: + if resolution is None: + resolution = 0.05 + elif resolution > 360: + raise ValueError(f"A resolution of greater than 360 was " + f"provided, but `ds` has a geographic CRS " + f"in {crs_units} units. Did you accidently " + f"provide a resolution in projected " + f"(i.e. metre) units?") + if buffer is None: + buffer = 0.12 + else: + if resolution is None: + resolution = 5000 + elif resolution < 1: + raise ValueError(f"A resolution of less than 1 was provided, " + f"but `ds` has a projected CRS in " + f"{crs_units} units. Did you accidently " + f"provide a resolution in geographic " + f"(degree) units?") + if buffer is None: + buffer = 12000 + + # Raise error if resolution is less than dataset resolution + dataset_res = ds.odc.geobox.resolution.x + if resolution < dataset_res: + raise ValueError(f"The resolution of the low-resolution tide " + f"modelling grid ({resolution:.2f}) is less " + f"than `ds`'s pixel resolution ({dataset_res:.2f}). " + f"This can cause extremely slow tide modelling " + f"performance. Please select provide a resolution " + f"greater than {dataset_res:.2f} using " + f"`pixel_tides`'s 'resolution' parameter.") + + # Create a new reduced resolution tide modelling grid after + # first buffering the grid + print(f"Creating reduced resolution {resolution} x {resolution} " + f"{crs_units} tide modelling array") + buffered_geobox = ds.odc.geobox.buffered(buffer) + rescaled_geobox = GeoBox.from_bbox( + bbox=buffered_geobox.boundingbox, resolution=resolution + ) + rescaled_ds = odc.geo.xr.xr_zeros(rescaled_geobox) + + # Flatten grid to 1D, then add time dimension + flattened_ds = rescaled_ds.stack(z=(x_dim, y_dim)) + flattened_ds = flattened_ds.expand_dims(dim={"time": time_coords.values}) + + # Model tides for each timestep + model = ( + "FES2014" if "model" not in model_tides_kwargs else model_tides_kwargs["model"] + ) + print(f"Modelling tides using {model} tide model") + tide_df = model_tides( + x=flattened_ds[x_dim], + y=flattened_ds[y_dim], + time=flattened_ds.time, + epsg=ds.odc.geobox.crs.epsg, + **model_tides_kwargs, + ) + + # Rename x and y coordinates to match satellite array + tide_df = tide_df.rename({"x": x_dim, "y": y_dim}, axis=1) + + # Insert modelled tide values back into flattened array, then unstack + # back to 3D (y, x, time) + tides_lowres = ( + + # Convert dataframe to xarray format + tide_df.set_index([x_dim, y_dim], append=True) + .to_xarray() + + # Re-index and transpose back into 3D + .tide_m.reindex_like(rescaled_ds) + .transpose("time", y_dim, x_dim) + .astype(np.float32) + ) + + # Optionally calculate and return quantiles rather than raw data + if calculate_quantiles is not None: + + print("Computing tide quantiles") + tides_lowres = tides_lowres.quantile(q=calculate_quantiles, dim="time") + reproject_dim = "quantile" + + else: + reproject_dim = "time" + + # Ensure CRS is present + tides_lowres = tides_lowres.odc.assign_crs(ds.odc.geobox.crs) + + # Reproject each timestep into original high resolution grid + if resample: + + print("Reprojecting tides into original array") + tides_highres = parallel_apply( + tides_lowres, + reproject_dim, + odc.algo.xr_reproject, + ds.odc.geobox.compat, + resample_method, + ) + + return tides_highres, tides_lowres + + else: + print("Returning low resolution tide array") + return tides_lowres + +def tidal_tag( + ds, + ebb_flow=False, + swap_dims=False, + tidepost_lat=None, + tidepost_lon=None, + return_tideposts=False, + **model_tides_kwargs, +): + """ + Takes an xarray.Dataset and returns the same dataset with a new + `tide_m` variable giving the height of the tide at the exact + moment of each satellite acquisition. + + The function models tides at the centroid of the dataset by default, + but a custom tidal modelling location can be specified using + `tidepost_lat` and `tidepost_lon`. + + The default settings use the FES2014 global tidal model, implemented + using the pyTMD Python package. FES2014 was produced by NOVELTIS, + LEGOS, CLS Space Oceanography Division and CNES. It is distributed + by AVISO, with support from CNES (http://www.aviso.altimetry.fr/). + + Parameters + ---------- + ds : xarray.Dataset + An xarray.Dataset object with x, y and time dimensions + ebb_flow : bool, optional + An optional boolean indicating whether to compute if the + tide phase was ebbing (falling) or flowing (rising) for each + observation. The default is False; if set to True, a new + `ebb_flow` variable will be added to the dataset with each + observation labelled with 'Ebb' or 'Flow'. + swap_dims : bool, optional + An optional boolean indicating whether to swap the `time` + dimension in the original xarray.Dataset to the new + `tide_m` variable. Defaults to False. + tidepost_lat, tidepost_lon : float or int, optional + Optional coordinates used to model tides. The default is None, + which uses the centroid of the dataset as the tide modelling + location. + return_tideposts : bool, optional + An optional boolean indicating whether to return the `tidepost_lat` + and `tidepost_lon` location used to model tides in addition to the + xarray.Dataset. Defaults to False. + **model_tides_kwargs : + Optional parameters passed to the `dea_tools.coastal.model_tides` + function. Important parameters include "model" and "directory", + used to specify the tide model to use and the location of its files. + + Returns + ------- + The original xarray.Dataset with a new `tide_m` variable giving + the height of the tide (and optionally, its ebb-flow phase) at the + exact moment of each satellite acquisition (if `return_tideposts=True`, + the function will also return the `tidepost_lon` and `tidepost_lat` + location used in the analysis). + + """ + + import odc.geo.xr + + # If custom tide modelling locations are not provided, use the + # dataset centroid + if not tidepost_lat or not tidepost_lon: + + tidepost_lon, tidepost_lat = ds.odc.geobox.geographic_extent.centroid.coords[0] + print( + f"Setting tide modelling location from dataset centroid: " + f"{tidepost_lon:.2f}, {tidepost_lat:.2f}" + ) + + else: + print( + f"Using user-supplied tide modelling location: " + f"{tidepost_lon:.2f}, {tidepost_lat:.2f}" + ) + + # Use tidal model to compute tide heights for each observation: + model = ( + "FES2014" if "model" not in model_tides_kwargs else model_tides_kwargs["model"] + ) + print(f"Modelling tides using {model} tidal model") + tide_df = model_tides( + x=tidepost_lon, + y=tidepost_lat, + time=ds.time, + epsg="EPSG:4326", + **model_tides_kwargs, + ) + + # If tides cannot be successfully modeled (e.g. if the centre of the + # xarray dataset is located is over land), raise an exception + if tide_df.tide_m.isnull().all(): + + raise ValueError( + f"Tides could not be modelled for dataset centroid located " + f"at {tidepost_lon:.2f}, {tidepost_lat:.2f}. This can occur if " + f"this coordinate occurs over land. Please manually specify " + f"a tide modelling location located over water using the " + f"`tidepost_lat` and `tidepost_lon` parameters." + ) + + # Assign tide heights to the dataset as a new variable + ds["tide_m"] = xr.DataArray(tide_df.tide_m, coords=[ds.time]) + + # Optionally calculate the tide phase for each observation + if ebb_flow: + + # Model tides for a time 15 minutes prior to each previously + # modelled satellite acquisition time. This allows us to compare + # tide heights to see if they are rising or falling. + print("Modelling tidal phase (e.g. ebb or flow)") + tide_pre_df = model_tides( + x=tidepost_lon, + y=tidepost_lat, + time=(ds.time - pd.Timedelta("15 min")), + epsg="EPSG:4326", + **model_tides_kwargs, + ) + + # Compare tides computed for each timestep. If the previous tide + # was higher than the current tide, the tide is 'ebbing'. If the + # previous tide was lower, the tide is 'flowing' + tidal_phase = [ + "Ebb" if i else "Flow" + for i in tide_pre_df.tide_m.values > tide_df.tide_m.values + ] + + # Assign tide phase to the dataset as a new variable + ds["ebb_flow"] = xr.DataArray(tidal_phase, coords=[ds.time]) + + # If swap_dims = True, make tide height the primary dimension + # instead of time + if swap_dims: + + # Swap dimensions and sort by tide height + ds = ds.swap_dims({"time": "tide_m"}) + ds = ds.sortby("tide_m") + ds = ds.drop_vars("time") + + if return_tideposts: + return ds, tidepost_lon, tidepost_lat + else: + return ds + + +def tidal_stats( + ds, + tidepost_lat=None, + tidepost_lon=None, + plain_english=True, + plot=True, + modelled_freq="2h", + linear_reg=False, + round_stats=3, + **model_tides_kwargs, +): + """ + Takes an xarray.Dataset and statistically compares the tides + modelled for each satellite observation against the full modelled + tidal range. This comparison can be used to evaluate whether the + tides observed by satellites (e.g. Landsat) are biased compared to + the natural tidal range (e.g. fail to observe either the highest or + lowest tides etc). + + For more information about the tidal statistics computed by this + function, refer to Figure 8 in Bishop-Taylor et al. 2018: + https://www.sciencedirect.com/science/article/pii/S0272771418308783#fig8 + + The function models tides at the centroid of the dataset by default, + but a custom tidal modelling location can be specified using + `tidepost_lat` and `tidepost_lon`. + + The default settings use the FES2014 global tidal model, implemented + using the pyTMD Python package. FES2014 was produced by NOVELTIS, + LEGOS, CLS Space Oceanography Division and CNES. It is distributed + by AVISO, with support from CNES (http://www.aviso.altimetry.fr/). + + Parameters + ---------- + ds : xarray.Dataset + An xarray.Dataset object with x, y and time dimensions + tidepost_lat, tidepost_lon : float or int, optional + Optional coordinates used to model tides. The default is None, + which uses the centroid of the dataset as the tide modelling + location. + plain_english : bool, optional + An optional boolean indicating whether to print a plain english + version of the tidal statistics to the screen. Defaults to True. + plot : bool, optional + An optional boolean indicating whether to plot how satellite- + observed tide heights compare against the full tidal range. + Defaults to True. + modelled_freq : str, optional + An optional string giving the frequency at which to model tides + when computing the full modelled tidal range. Defaults to '2h', + which computes a tide height for every two hours across the + temporal extent of `ds`. + linear_reg: bool, optional + Experimental: whether to return linear regression stats that + assess whether dstellite-observed and all available tides show + any decreasing or increasing trends over time. Not currently + recommended as all observed regressions always return as + significant due to far larger sample size. + round_stats : int, optional + The number of decimal places used to round the output statistics. + Defaults to 3. + **model_tides_kwargs : + Optional parameters passed to the `dea_tools.coastal.model_tides` + function. Important parameters include "model" and "directory", + used to specify the tide model to use and the location of its files. + + Returns + ------- + A pandas.Series object containing the following statistics: + + tidepost_lat: latitude used for modelling tide heights + tidepost_lon: longitude used for modelling tide heights + observed_min_m: minimum tide height observed by the satellite + all_min_m: minimum tide height from all available tides + observed_max_m: maximum tide height observed by the satellite + all_max_m: maximum tide height from all available tides + observed_range_m: tidal range observed by the satellite + all_range_m: full astronomical tidal range based on all + available tides + spread_m: proportion of the full astronomical tidal range observed + by the satellite (see Bishop-Taylor et al. 2018) + low_tide_offset: proportion of the lowest tides never observed + by the satellite (see Bishop-Taylor et al. 2018) + high_tide_offset: proportion of the highest tides never observed + by the satellite (see Bishop-Taylor et al. 2018) + + If `linear_reg = True`, the output will also contain: + + observed_slope: slope of any relationship between observed tide + heights and time + all_slope: slope of any relationship between all available tide + heights and time + observed_pval: significance/p-value of any relationship between + observed tide heights and time + all_pval: significance/p-value of any relationship between + all available tide heights and time + + """ + + # Model tides for each observation in the supplied xarray object + ds_tides, tidepost_lon, tidepost_lat = tidal_tag( + ds, + tidepost_lat=tidepost_lat, + tidepost_lon=tidepost_lon, + return_tideposts=True, + **model_tides_kwargs, + ) + + # Drop spatial ref for nicer plotting + if "spatial_ref" in ds_tides: + ds_tides = ds_tides.drop_vars("spatial_ref") + + # Generate range of times covering entire period of satellite record + all_timerange = pd.date_range( + start=ds_tides.time.min().item(), + end=ds_tides.time.max().item(), + freq=modelled_freq, + ) + + # Model tides for each timestep + all_tides_df = model_tides( + x=tidepost_lon, + y=tidepost_lat, + time=all_timerange, + epsg="EPSG:4326", + **model_tides_kwargs, + ) + + # Get coarse statistics on all and observed tidal ranges + obs_mean = ds_tides.tide_m.mean().item() + all_mean = all_tides_df.tide_m.mean() + obs_min, obs_max = ds_tides.tide_m.quantile([0.0, 1.0]).values + all_min, all_max = all_tides_df.tide_m.quantile([0.0, 1.0]).values + + # Calculate tidal range + obs_range = obs_max - obs_min + all_range = all_max - all_min + + # Calculate Bishop-Taylor et al. 2018 tidal metrics + spread = obs_range / all_range + low_tide_offset = abs(all_min - obs_min) / all_range + high_tide_offset = abs(all_max - obs_max) / all_range + + # Extract x (time in decimal years) and y (distance) values + all_x = ( + all_tides_df.index.year + + ((all_tides_df.index.dayofyear - 1) / 365) + + ((all_tides_df.index.hour - 1) / 24) + ) + all_y = all_tides_df.tide_m.values.astype(np.float32) + time_period = all_x.max() - all_x.min() + + # Extract x (time in decimal years) and y (distance) values + obs_x = ( + ds_tides.time.dt.year + + ((ds_tides.time.dt.dayofyear - 1) / 365) + + ((ds_tides.time.dt.hour - 1) / 24) + ) + obs_y = ds_tides.tide_m.values.astype(np.float32) + + + # Compute linear regression + obs_linreg = stats.linregress(x=obs_x, y=obs_y) + all_linreg = stats.linregress(x=all_x, y=all_y) + + if plain_english: + + print( + f"\n{spread:.0%} of the {all_range:.2f} m modelled astronomical " + f"tidal range is observed at this location.\nThe lowest " + f"{low_tide_offset:.0%} and highest {high_tide_offset:.0%} " + f"of astronomical tides are never observed.\n" + ) + + if linear_reg: + + if obs_linreg.pvalue > 0.05: + print( + f"Observed tides show no significant trends " + f"over the ~{time_period:.0f} year period." + ) + else: + obs_slope_desc = "decrease" if obs_linreg.slope < 0 else "increase" + print( + f"Observed tides {obs_slope_desc} significantly " + f"(p={obs_linreg.pvalue:.3f}) over time by " + f"{obs_linreg.slope:.03f} m per year (i.e. a " + f"~{time_period * obs_linreg.slope:.2f} m " + f"{obs_slope_desc} over the ~{time_period:.0f} year period)." + ) + + if all_linreg.pvalue > 0.05: + print( + f"All tides show no significant trends " + f"over the ~{time_period:.0f} year period." + ) + else: + all_slope_desc = "decrease" if all_linreg.slope < 0 else "increase" + print( + f"All tides {all_slope_desc} significantly " + f"(p={all_linreg.pvalue:.3f}) over time by " + f"{all_linreg.slope:.03f} m per year (i.e. a " + f"~{time_period * all_linreg.slope:.2f} m " + f"{all_slope_desc} over the ~{time_period:.0f} year period)." + ) + + if plot: + + # Create plot and add all time and observed tide data + fig, ax = plt.subplots(figsize=(10, 5)) + all_tides_df.tide_m.plot(ax=ax, alpha=0.4) + ds_tides.tide_m.plot.line( + ax=ax, marker="o", linewidth=0.0, color="black", markersize=2 + ) + + # Add horizontal lines for spread/offsets + ax.axhline(obs_min, color="black", linestyle=":", linewidth=1) + ax.axhline(obs_max, color="black", linestyle=":", linewidth=1) + ax.axhline(all_min, color="black", linestyle=":", linewidth=1) + ax.axhline(all_max, color="black", linestyle=":", linewidth=1) + + # Add text annotations for spread/offsets + ax.annotate( + f" High tide\n offset ({high_tide_offset:.0%})", + xy=(all_timerange.max(), np.mean([all_max, obs_max])), + va="center", + ) + ax.annotate( + f" Spread\n ({spread:.0%})", + xy=(all_timerange.max(), np.mean([obs_min, obs_max])), + va="center", + ) + ax.annotate( + f" Low tide\n offset ({low_tide_offset:.0%})", + xy=(all_timerange.max(), np.mean([all_min, obs_min])), + ) + + # Remove top right axes and add labels + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.set_ylabel("Tide height (m)") + ax.set_xlabel("") + ax.margins(x=0.015) + + # Export pandas.Series containing tidal stats + output_stats = { + "tidepost_lat": tidepost_lat, + "tidepost_lon": tidepost_lon, + "observed_mean_m": obs_mean, + "all_mean_m": all_mean, + "observed_min_m": obs_min, + "all_min_m": all_min, + "observed_max_m": obs_max, + "all_max_m": all_max, + "observed_range_m": obs_range, + "all_range_m": all_range, + "spread": spread, + "low_tide_offset": low_tide_offset, + "high_tide_offset": high_tide_offset, + } + + if linear_reg: + output_stats.update( + { + "observed_slope": obs_linreg.slope, + "all_slope": all_linreg.slope, + "observed_pval": obs_linreg.pvalue, + "all_pval": all_linreg.pvalue, + } + ) + + return pd.Series(output_stats).round(round_stats) + + +def transect_distances(transects_gdf, lines_gdf, mode='distance'): + """ + Take a set of transects (e.g. shore-normal beach survey lines), and + determine the distance along the transect to each object in a set of + lines (e.g. shorelines). Distances are measured in the CRS of the + input datasets. + + For coastal applications, transects should be drawn from land to + water (with the first point being on land so that it can be used + as a consistent location from which to measure distances. + + The distance calculation can be performed using two modes: + - 'distance': Distances are measured from the start of the + transect to where it intersects with each line. Any transect + that intersects a line more than once is ignored. This mode is + useful for measuring e.g. the distance to the shoreline over + time from a consistent starting location. + - 'width' Distances are measured between the first and last + intersection between a transect and each line. Any transect + that intersects a line only once is ignored. This is useful + for e.g. measuring the width of a narrow area of coastline over + time, e.g. the neck of a spit or tombolo. + + Parameters + ---------- + transects_gdf : geopandas.GeoDataFrame + A GeoDataFrame containing one or multiple vector profile lines. + The GeoDataFrame's index column will be used to name the rows in + the output distance table. + lines_gdf : geopandas.GeoDataFrame + A GeoDataFrame containing one or multiple vector line features + that intersect the profile lines supplied to `transects_gdf`. + The GeoDataFrame's index column will be used to name the columns + in the output distance table. + mode : string, optional + Whether to use 'distance' (for measuring distances from the + start of a profile) or 'width' mode (for measuring the width + between two profile intersections). See docstring above for more + info; defaults to 'distance'. + + Returns + ------- + distance_df : pandas.DataFrame + A DataFrame containing distance measurements for each profile + line (rows) and line feature (columns). + """ + + import warnings + from shapely.errors import ShapelyDeprecationWarning + from shapely.geometry import Point + + def _intersect_dist(transect_gdf, lines_gdf, mode=mode): + """ + Take an individual transect, and determine the distance along + the transect to each object in a set of lines (e.g. shorelines). + """ + + # Identify intersections between transects and lines + intersect_points = lines_gdf.apply( + lambda x: x.geometry.intersection(transect_gdf.geometry), axis=1) + + # In distance mode, identify transects with one intersection only, + # and use this as the end point and the start of the transect as the + # start point when measuring distances + if mode == 'distance': + start_point = Point(transect_gdf.geometry.coords[0]) + point_df = intersect_points.apply( + lambda x: pd.Series({'start': start_point, 'end': x}) + if x.type == 'Point' + else pd.Series({'start': None, 'end': None})) + + # In width mode, identify transects with multiple intersections, and + # use the first intersection as the start point and the second + # intersection for the end point when measuring distances + if mode == 'width': + point_df = intersect_points.apply( + lambda x: pd.Series({'start': x.geoms[0], 'end': x.geoms[-1]}) + if x.type == 'MultiPoint' + else pd.Series({'start': None, 'end': None})) + + # Calculate distances between valid start and end points + distance_df = point_df.apply( + lambda x: x.start.distance(x.end) if x.start else None, axis=1) + + return distance_df + + # Run code after ignoring Shapely pre-v2.0 warnings + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=ShapelyDeprecationWarning) + + # Assert that both datasets use the same CRS + assert transects_gdf.crs == lines_gdf.crs, ('Please ensure both ' + 'input datasets use the same CRS.') + + # Run distance calculations + distance_df = transects_gdf.apply( + lambda x: _intersect_dist(x, lines_gdf), axis=1) + + return pd.DataFrame(distance_df) + + +def get_coastlines(bbox: tuple, + crs="EPSG:4326", + layer="shorelines", + drop_wms=True) -> gpd.GeoDataFrame: + """ + Get DE Africa Coastlines data for a provided bounding box using WFS. + + For a full description of the DE Africa Coastlines dataset, refer to the + official Digital Earth Africa product description: + + Parameters + ---------- + bbox : (xmin, ymin, xmax, ymax), or geopandas object + Bounding box expressed as a tuple. Alternatively, a bounding + box can be automatically extracted by suppling a + geopandas.GeoDataFrame or geopandas.GeoSeries. + crs : str, optional + Optional CRS for the bounding box. This is ignored if `bbox` + is provided as a geopandas object. + layer : str, optional + Which DE Africa Coastlines layer to load. Options include the annual + shoreline vectors ("shorelines") and the rates of change + statistics points ("statistics"). Defaults to "shorelines". + drop_wms : bool, optional + Whether to drop WMS-specific attribute columns from the data. + These columns are used for visualising the dataset on DE Africa Maps, + and are unlikely to be useful for scientific analysis. Defaults + to True. + + Returns + ------- + gpd.GeoDataFrame + A GeoDataFrame containing shoreline or point features and + associated metadata. + """ + + # If bbox is a geopandas object, convert to bbox. + try: + crs = str(bbox.crs) + bbox = bbox.total_bounds + except: + pass + + # Get the available layers in the coastlines:DEAfrica_Coastlines group. + describe_layer_url = "https://geoserver.digitalearth.africa/geoserver/wms?service=WMS&version=1.1.1&request=DescribeLayer&layers=coastlines:DEAfrica_Coastlines&outputFormat=application/json" + describe_layer_response = requests.get(describe_layer_url).json() + available_layers = [layer["layerName"] for layer in describe_layer_response['layerDescriptions']] + + # Get the layer name. + if layer == "shorelines": + layer_name = [i for i in available_layers if "shorelines" in i] + else: + layer_name = [i for i in available_layers if "rates_of_change" in i] + + # Query WFS. + wfs = WebFeatureService(url=WFS_ADDRESS, version="1.1.0") + response = wfs.getfeature(typename=layer_name, + bbox=tuple(bbox) + (crs,), + outputFormat="json") + + # Load data as a geopandas.GeoDataFrame. + coastlines_gdf = gpd.read_file(response) + + # Clip to extent of bounding box. + extent = gpd.GeoSeries(box(*bbox), crs=crs).to_crs(coastlines_gdf.crs) + coastlines_gdf = coastlines_gdf.clip(extent) + + # Optionally drop WMS-specific columns. + if drop_wms: + coastlines_gdf = coastlines_gdf.loc[:, ~coastlines_gdf.columns.str.contains("wms_")] + + return coastlines_gdf + diff --git a/deafrica_tools/dask.py b/deafrica_tools/dask.py new file mode 100644 index 0000000..c29ed54 --- /dev/null +++ b/deafrica_tools/dask.py @@ -0,0 +1,106 @@ +""" +Functions for simplifying the creation of a local dask cluster. +""" + +from importlib.util import find_spec +import os +import dask +from aiohttp import ClientConnectionError +from datacube.utils.dask import start_local_dask +from datacube.utils.rio import configure_s3_access + +_HAVE_PROXY = bool(find_spec('jupyter_server_proxy')) +_IS_AWS = ('AWS_ACCESS_KEY_ID' in os.environ or + 'AWS_DEFAULT_REGION' in os.environ) + + +def create_local_dask_cluster(spare_mem='3Gb', display_client=True, return_client=False): + """ + Using the datacube utils function `start_local_dask`, generate + a local dask cluster. Automatically detects if on AWS or NCI. + + Parameters + ---------- + spare_mem : String, optional + The amount of memory, in Gb, to leave for the notebook to run. + This memory will not be used by the cluster. e.g '3Gb' + display_client : Bool, optional + An optional boolean indicating whether to display a summary of + the dask client, including a link to monitor progress of the + analysis. Set to False to hide this display. + return_client : Bool, optional + An optional boolean indicating whether to return the dask client + object. + + """ + + if _HAVE_PROXY: + # Configure dashboard link to go over proxy + prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/') + dask.config.set({"distributed.dashboard.link": + prefix + "proxy/{port}/status"}) + + # Start up a local cluster + client = start_local_dask(mem_safety_margin=spare_mem) + + if _IS_AWS: + # Configure GDAL for s3 access + configure_s3_access(aws_unsigned=True, + client=client) + + # Show the dask cluster settings + if display_client: + from IPython.display import display + display(client) + + # return the client as an object + if return_client: + return client + + +try: + from dask_gateway import Gateway + + def create_dask_gateway_cluster(profile='r5_L', workers=2): + """ + Create a cluster in our internal dask cluster. + + Parameters + ---------- + profile : str + Possible values are: + - r5_L (2 cores, 15GB memory) + - r5_XL (4 cores, 31GB memory) + - r5_2XL (8 cores, 63GB memory) + - r5_4XL (16 cores, 127GB memory) + + workers : int + Number of workers in the cluster. + """ + try: + gateway = Gateway() + + # Close any existing clusters + cluster_names = gateway.list_clusters() + if len(cluster_names) > 0: + print("Cluster(s) still running:", cluster_names) + for n in cluster_names: + cluster = gateway.connect(n.name) + cluster.shutdown() + + options = gateway.cluster_options() + options['profile'] = profile + + # limit username to alphanumeric characters + # kubernetes pods won't launch if labels contain anything other than [a-Z, -, _] + options['jupyterhub_user'] = ''.join(c if c.isalnum() else '-' for c in os.getenv('JUPYTERHUB_USER')) + + cluster = gateway.new_cluster(options) + cluster.scale(workers) + return cluster + except ClientConnectionError: + raise ConnectionError("access to dask gateway cluster unauthorized") + +except ImportError: + def create_dask_gateway_cluster(*args, **kwargs): + raise NotImplementedError \ No newline at end of file diff --git a/deafrica_tools/datahandling.py b/deafrica_tools/datahandling.py new file mode 100644 index 0000000..085952a --- /dev/null +++ b/deafrica_tools/datahandling.py @@ -0,0 +1,1523 @@ +""" +Functions for loading and handling Digital Earth Africa data. +""" + +# Import required packages +import os +from osgeo import gdal +import requests +import zipfile +import warnings +import numpy as np +import xarray as xr +import pandas as pd +import datetime +import pytz + +from collections import Counter +from datacube.utils import masking +from scipy.ndimage import binary_dilation +from odc.algo import mask_cleanup +from copy import deepcopy +import odc.algo + +from skimage.morphology import binary_erosion,binary_dilation,disk +from scipy.ndimage.filters import uniform_filter +from scipy.ndimage.measurements import variance +from datetime import datetime +from dateutil import parser +from deafrica_tools.bandindices import calculate_indices + +def _dc_query_only(**kw): + """ + Remove load-only parameters, the rest + can be passed to Query + + Returns + ======= + + dict of query parameters + """ + + def _impl( + measurements=None, + output_crs=None, + resolution=None, + resampling=None, + skip_broken_datasets=None, + dask_chunks=None, + fuse_func=None, + align=None, + datasets=None, + progress_cbk=None, + group_by=None, + **query, + ): + return query + + return _impl(**kw) + + +def _common_bands(dc, products): + """ + Takes a list of products and returns a list of measurements/bands + that are present in all products + Returns + ------- + List of band names + """ + common = None + bands = None + + for p in products: + p = dc.index.products.get_by_name(p) + if common is None: + common = set(p.measurements) + bands = list(p.measurements) + else: + common = common.intersection(set(p.measurements)) + return [band for band in bands if band in common] + + +def load_ard( + dc, + products=None, + min_gooddata=0.0, + categories_to_mask_ls=dict( + cloud="high_confidence", cloud_shadow="high_confidence" + ), + categories_to_mask_s2=[ + "cloud high probability", + "cloud medium probability", + "thin cirrus", + "cloud shadows", + "saturated or defective", + ], + categories_to_mask_s1=["invalid data"], + mask_filters=None, + mask_pixel_quality=True, + ls7_slc_off=True, + predicate=None, + dtype="auto", + verbose=True, + **kwargs, +): + """ + Loads analysis ready data. + + Loads and combines Landsat USGS Collections 2, Sentinel-2, and Sentinel-1 for + multiple sensors (i.e. ls5t, ls7e, ls8c and ls9 for Landsat; s2a and s2b for Sentinel-2), + optionally applies pixel quality masks, and drops time steps that + contain greater than a minimum proportion of good quality (e.g. non- + cloudy or shadowed) pixels. + + The function supports loading the following DE Africa products: + + Landsat: + * ls5_sr ('sr' denotes surface reflectance) + * ls7_sr + * ls8_sr + * ls9_sr + * ls5_st ('st' denotes surface temperature) + * ls7_st + * ls8_st + * ls9_st + + Sentinel-2: + * s2_l2a + + Sentinel-1: + * s1_rtc + + Last modified: Feb 2021 + + Parameters + ---------- + dc : datacube Datacube object + The Datacube to connect to, i.e. `dc = datacube.Datacube()`. + This allows you to also use development datacubes if required. + products : list + A list of product names to load data from. For example: + + * Landsat C2: ``['ls5_sr', 'ls7_sr', 'ls8_sr', 'ls9_sr']`` + * Sentinel-2: ``['s2_l2a']`` + * Sentinel-1: ``['s1_rtc']`` + + min_gooddata : float, optional + An optional float giving the minimum percentage of good quality + pixels required for a satellite observation to be loaded. + Defaults to 0.0 which will return all observations regardless of + pixel quality (set to e.g. 0.99 to return only observations with + more than 99% good quality pixels). + categories_to_mask_ls : dict, optional + An optional dictionary that is used to identify poor quality pixels + for masking. This mask is used for both masking out low + quality pixels (e.g. cloud or shadow), and for dropping + observations entirely based on the `min_gooddata` calculation. + categories_to_mask_s2 : list, optional + An optional list of Sentinel-2 Scene Classification Layer (SCL) names + that identify poor quality pixels for masking. + categories_to_mask_s1 : list, optional + An optional list of Sentinel-1 mask names that identify poor + quality pixels for masking. + mask_filters : iterable of tuples, optional + Iterable tuples of morphological operations - ("", ) + to apply on mask, where: + + operation: string, can be one of these morphological operations: + * ``'closing'`` = remove small holes in cloud - morphological closing + * ``'opening'`` = shrinks away small areas of the mask + * ``'dilation'`` = adds padding to the mask + * ``'erosion'`` = shrinks bright regions and enlarges dark regions + + radius: int + e.g. ``mask_filters=[('erosion', 5),("opening", 2),("dilation", 2)]`` + mask_pixel_quality : bool, optional + An optional boolean indicating whether to apply the poor data + mask to all observations that were not filtered out for having + less good quality pixels than ``min_gooddata``. E.g. if + ``min_gooddata=0.99``, the filtered observations may still contain + up to 1% poor quality pixels. The default of ``False`` simply + returns the resulting observations without masking out these + pixels; ``True`` masks them and sets them to NaN using the poor data + mask. This will convert numeric values to floating point values + which can cause memory issues, set to False to prevent this. + ls7_slc_off : bool, optional + An optional boolean indicating whether to include data from + after the Landsat 7 SLC failure (i.e. SLC-off). Defaults to + ``True``, which keeps all Landsat 7 observations > May 31 2003. + predicate : function, optional + An optional function that can be passed in to restrict the + datasets that are loaded by the function. A filter function + should take a `datacube.model.Dataset` object as an input (i.e. + as returned from `dc.find_datasets`), and return a boolean. + For example, a filter function could be used to return True on + only datasets acquired in January: + ``dataset.time.begin.month == 1`` + dtype : string, optional + An optional parameter that controls the data type/dtype that + layers are coerced to after loading. Valid values: ''`native`'', + ``'auto'``, ``'float{16|32|64}'``. + When ``'auto'`` is used, the data will be + converted to ``'float32'`` if masking is used, otherwise data will + be returned in the native data type of the data. Be aware that + if data is loaded in its native dtype, nodata and masked + pixels will be returned with the data's native nodata value + (typically ``-999``), not ``NaN``. + NOTE: If loading Landsat, the data is automatically rescaled so + 'native' dtype will return a value error. + verbose : bool, optional + If True, print progress statements during loading + **kwargs : dict, optional + A set of keyword arguments to ``dc.load`` that define the + spatiotemporal query used to extract data. This typically + includes ``measurements``, ``x`, ``y``, ``time``, ``resolution``, + ``resampling``, ``group_by`` and ``crs``. Keyword arguments can + either be listed directly in the ``load_ard`` call like any + other parameter (e.g. ``measurements=['red']``), or by + passing in a query kwarg dictionary (e.g. ``**query``). For a + list of possible options, see the ``dc.load`` documentation: + https://datacube-core.readthedocs.io/en/latest/dev/api/generate/datacube.Datacube.load.html + + Returns + ------- + combined_ds : xarray Dataset + An xarray dataset containing only satellite observations that + contains greater than `min_gooddata` proportion of good quality + pixels. + + """ + + ######### + # Setup # + ######### + # prevent function altering original query object + kwargs = deepcopy(kwargs) + + # We deal with `dask_chunks` separately + dask_chunks = kwargs.pop("dask_chunks", None) + requested_measurements = kwargs.pop("measurements", None) + + # Warn user if they combine lazy load with min_gooddata + if verbose: + if (min_gooddata > 0.0) and dask_chunks is not None: + warnings.warn( + "Setting 'min_gooddata' percentage to > 0.0 " + "will cause dask arrays to compute when " + "loading pixel-quality data to calculate " + "'good pixel' percentage. This can " + "slow the return of your dataset." + ) + + # Verify that products were provided and determine if Sentinel-2 + # or Landsat data is being loaded + if not products: + raise ValueError( + "Please provide a list of product names to load data from. " + "Valid options are: Landsat C2 SR: ['ls5_sr', 'ls7_sr', 'ls8_sr', 'ls9_sr'], or " + "Landsat C2 ST: ['ls5_st', 'ls7_st', 'ls8_st', 'ls9_st'], or " + "Sentinel-2: ['s2_l2a'], or" + "Sentinel-1: ['s1_rtc'], or" + ) + + # convert products to list if user passed as a string + if type(products) == str: + products=[products] + + if all(["ls" in product for product in products]): + product_type = "ls" + elif all(["s2" in product for product in products]): + product_type = "s2" + elif all(["s1" in product for product in products]): + product_type = "s1" + + # check if the landsat product is surface temperature + st = False + if (product_type == "ls") & (all(["st" in product for product in products])): + st = True + + # Check some parameters before proceeding + if (product_type == "ls") & (dtype == "native"): + raise ValueError( + "Cannot load Landsat bands in native dtype " + "as values require rescaling which converts dtype to float" + ) + + if product_type == "ls": + if any(k in categories_to_mask_ls for k in ("cirrus", "cirrus_confidence")): + raise ValueError( + "'cirrus' categories for the pixel quality mask" + " are not supported by load_ard" + ) + + # If `measurements` are specified but do not include pixel quality bands, + # add these to `measurements` according to collection + if product_type == "ls": + if verbose: + print("Using pixel quality parameters for USGS Collection 2") + fmask_band = "pixel_quality" + + elif product_type == "s2": + if verbose: + print("Using pixel quality parameters for Sentinel 2") + fmask_band = "SCL" + + elif product_type == "s1": + if verbose: + print("Using pixel quality parameters for Sentinel 1") + fmask_band = "mask" + + measurements = requested_measurements.copy() if requested_measurements else None + + # define a list of acceptable aliases to load landsat. We can't rely on 'common' + # measurements as native band names have the same name for different measurements. + ls_aliases = ["pixel_quality", "radiometric_saturation"] + if st: + ls_aliases = [ + "surface_temperature", + "surface_temperature_quality", + "atmospheric_transmittance", + "thermal_radiance", + "emissivity", + "emissivity_stddev", + "cloud_distance", + "upwell_radiance", + "downwell_radiance", + ] + ls_aliases + else: + ls_aliases = ["red", "green", "blue", "nir", "swir_1", "swir_2"] + ls_aliases + + if measurements is not None: + if product_type == "ls": + + # check we aren't loading aerosol bands from LS8 + aerosol_bands = [ + "aerosol_qa", + "qa_aerosol", + "atmos_opacity", + "coastal_aerosol", + "SR_QA_AEROSOL", + ] + if any(b in aerosol_bands for b in measurements): + raise ValueError( + "load_ard doesn't support loading aerosol or " + "atmospeheric opacity related bands " + "for Landsat, instead use dc.load()" + ) + + # check measurements are in acceptable aliases list for landsat + if set(measurements).issubset(ls_aliases): + pass + else: + raise ValueError( + "load_ard does not support all band aliases for Landsat, " + "use only the following band names to load Landsat data: " + + str(ls_aliases) + ) + + # Deal with "load all" case: pick a set of bands common across + # all products + if measurements is None: + if product_type == "ls": + measurements = ls_aliases + else: + measurements = _common_bands(dc, products) + + # If `measurements` are specified but do not include pq, add. + if measurements: + if fmask_band not in measurements: + measurements.append(fmask_band) + + # Get list of data and mask bands so that we can later exclude + # mask bands from being masked themselves (also handle the case of rad_sat) + data_bands = [ + band + for band in measurements + if band not in (fmask_band, "radiometric_saturation") + ] + mask_bands = [band for band in measurements if band not in data_bands] + + ################# + # Find datasets # + ################# + + # Pull out query params only to pass to dc.find_datasets + query = _dc_query_only(**kwargs) + + # Extract datasets for each product using subset of dcload_kwargs + dataset_list = [] + + # Get list of datasets for each product + if verbose: + print("Finding datasets") + for product in products: + + # Obtain list of datasets for product + if verbose: + print(f" {product}") + + if product_type == "ls": + # handle LS seperately to S2/S1 due to collection_category + # force the user to load Tier 1 + datasets = dc.find_datasets( + product=product, collection_category='T1', **query + ) + else: + datasets = dc.find_datasets(product=product, **query) + + # Remove Landsat 7 SLC-off observations if ls7_slc_off=False + if not ls7_slc_off and product in ["ls7_sr"]: + if verbose: + print(" Ignoring SLC-off observations for ls7") + datasets = [ + i + for i in datasets + if i.time.begin < datetime.datetime(2003, 5, 31, tzinfo=pytz.UTC) + ] + + # Add any returned datasets to list + dataset_list.extend(datasets) + + # Raise exception if no datasets are returned + if len(dataset_list) == 0: + raise ValueError( + "No data available for query: ensure that " + "the products specified have data for the " + "time and location requested" + ) + + # If predicate is specified, use this function to filter the list + # of datasets prior to load (this now redundant as dc.load now supports + # a predicate filter) + if predicate: + if verbose: + print(f"Filtering datasets using filter function") + dataset_list = [ds for ds in dataset_list if predicate(ds)] + + # Raise exception if filtering removes all datasets + if len(dataset_list) == 0: + raise ValueError("No data available after filtering with " "filter function") + + ############# + # Load data # + ############# + + # Note we always load using dask here so that + # we can lazy load data before filtering by good data + ds = dc.load( + datasets=dataset_list, + measurements=measurements, + dask_chunks={} if dask_chunks is None else dask_chunks, + **kwargs, + ) + #print(ds) + #################### + # Filter good data # + #################### + + # need to distinguish between products due to different + # pq band properties + + # collection 2 USGS + if product_type == "ls": + mask, _ = masking.create_mask_value( + ds[fmask_band].attrs["flags_definition"], **categories_to_mask_ls + ) + + pq_mask = (ds[fmask_band] & mask) != 0 + + # only run if data bands are present + if len(data_bands) > 0: + + # identify pixels that will become negative after rescaling (but not 0 values) + invalid = ( + ((ds[data_bands] < (-1.0 * -0.2 / 0.0000275)) & (ds[data_bands] > 0)) + .to_array(dim="band") + .any(dim="band") + ) + + #merge masks + pq_mask = np.logical_or(pq_mask, pq_mask) + + # sentinel 2 + if product_type == "s2": + pq_mask = odc.algo.enum_to_bool(mask=ds[fmask_band], + categories=categories_to_mask_s2) + + # sentinel 1 + if product_type == "s1": + pq_mask = odc.algo.enum_to_bool(mask=ds[fmask_band], + categories=categories_to_mask_s1) + #print(pq_mask) + # The good data percentage calculation has to load in all `fmask` + # data, which can be slow. If the user has chosen no filtering + # by using the default `min_gooddata = 0`, we can skip this step + # completely to save processing time + if min_gooddata > 0.0: + + # Compute good data for each observation as % of total pixels. + # Inveerting the pq_mask for this because cloud=True in pq_mask + # and we want to sum good pixels + if verbose: + print("Counting good quality pixels for each time step") + data_perc = (~pq_mask).sum(axis=[1, 2], dtype="int32") / ( + pq_mask.shape[1] * pq_mask.shape[2] + ) + + keep = (data_perc >= min_gooddata).persist() + + # Filter by `min_gooddata` to drop low quality observations + total_obs = len(ds.time) + ds = ds.sel(time=keep) + pq_mask = pq_mask.sel(time=keep) + + if verbose: + print( + f"Filtering to {len(ds.time)} out of {total_obs} " + f"time steps with at least {min_gooddata:.1%} " + f"good quality pixels" + ) + + # morpholigcal filtering on cloud masks + if (mask_filters is not None) & (mask_pixel_quality): + if verbose: + print(f"Applying morphological filters to pq mask {mask_filters}") + pq_mask = mask_cleanup(pq_mask, mask_filters=mask_filters) + + ############### + # Apply masks # + ############### + + # Generate good quality data mask + mask = None + if mask_pixel_quality: + if verbose: + print("Applying pixel quality/cloud mask") + mask = pq_mask + + # Split into data/masks bands, as conversion to float and masking + # should only be applied to data bands + ds_data = ds[data_bands] + ds_masks = ds[mask_bands] + + # Remove sentinel-2 pixels valued 1 (scene edges, terrain shadow) + if product_type == "s2": + valid_data_mask = (ds_data > 1).to_array(dim="band").all(dim="band") + ds_data = odc.algo.keep_good_only(ds_data, where=valid_data_mask) + + # Mask data if either of the above masks were generated + if mask is not None: + ds_data = odc.algo.erase_bad(ds_data, where=mask) + + # Automatically set dtype to either native or float32 depending + # on whether masking was requested + if dtype == "auto": + dtype = "native" if mask is None else "float32" + + # Set nodata values using odc.algo tools to reduce peak memory + # use when converting data dtype + if dtype != "native": + ds_data = odc.algo.to_float(ds_data, dtype=dtype) + + # Put data and mask bands back together + attrs = ds.attrs + ds = xr.merge([ds_data, ds_masks]) + ds.attrs.update(attrs) + + ############### + # Return data # + ############### + + # Drop bands not originally requested by user + if requested_measurements: + ds = ds[requested_measurements] + + # Apply the scale and offset factors to Collection 2 Landsat. We need + # different factors for different bands. Also handle the case where + # masking_pixel_quaity = False, in which case the dtype is still + # in int, so we convert it to float + if product_type == "ls": + if verbose: + print("Re-scaling Landsat C2 data") + + sr_bands = ["red", "green", "blue", "nir", "swir_1", "swir_2"] + radiance_bands = ["thermal_radiance", "upwell_radiance", "downwell_radiance"] + trans_emiss = ["atmospheric_transmittance", "emissivity", "emissivity_stddev"] + qa = ["pixel_quality", "radiometric_saturation"] + + if mask_pixel_quality == False: + # set nodata to NaNs before rescaling + # in the case where masking hasn't already done this + for band in ds.data_vars: + if band not in qa: + ds[band] = odc.algo.to_f32(ds[band]) + + for band in ds.data_vars: + if band == "cloud_distance": + ds[band] = 0.01 * ds[band] + + if band == "surface_temperature_quality": + ds[band] = 0.01 * ds[band] + + if band in radiance_bands: + ds[band] = 0.001 * ds[band] + + if band in trans_emiss: + ds[band] = 0.0001 * ds[band] + + if band in sr_bands: + ds[band] = 2.75e-5 * ds[band] - 0.2 + + if band == "surface_temperature": + ds[band] = ds[band] * 0.00341802 + 149.0 + + # add back attrs that are lost during scaling calcs + for band in ds.data_vars: + ds[band].attrs.update(attrs) + + # If user supplied dask_chunks, return data as a dask array without + # actually loading it in + if dask_chunks is not None: + if verbose: + print(f"Returning {len(ds.time)} time steps as a dask array") + return ds + else: + if verbose: + print(f"Loading {len(ds.time)} time steps") + return ds.compute() + + +def array_to_geotiff( + fname, data, geo_transform, projection, nodata_val=0, dtype=gdal.GDT_Float32 +): + """ + Create a single band GeoTIFF file with data from an array. + + Because this works with simple arrays rather than xarray datasets + from DEA, it requires geotransform info (`(upleft_x, x_size, + x_rotation, upleft_y, y_rotation, y_size)`) and projection data + (in "WKT" format) for the output raster. These are typically + obtained from an existing raster using the following GDAL calls: + + >>> from osgeo import gdal + >>> gdal_dataset = gdal.Open(raster_path) + >>> geotrans = gdal_dataset.GetGeoTransform() + >>> prj = gdal_dataset.GetProjection() + + or alternatively, directly from an xarray dataset: + + >>> geotrans = xarraydataset.geobox.transform.to_gdal() + >>> prj = xarraydataset.geobox.crs.wkt + + + Parameters + ---------- + fname : str + Output geotiff file path including extension + data : numpy array + Input array to export as a geotiff + geo_transform : tuple + Geotransform for output raster; e.g. `(upleft_x, x_size, + x_rotation, upleft_y, y_rotation, y_size)` + projection : str + Projection for output raster (in "WKT" format) + nodata_val : int, optional + Value to convert to nodata in the output raster; default 0 + dtype : gdal dtype object, optional + Optionally set the dtype of the output raster; can be + useful when exporting an array of float or integer values. + Defaults to `gdal.GDT_Float32` + + """ + + # Set up driver + driver = gdal.GetDriverByName("GTiff") + + # Create raster of given size and projection + rows, cols = data.shape + dataset = driver.Create(fname, cols, rows, 1, dtype) + dataset.SetGeoTransform(geo_transform) + dataset.SetProjection(projection) + + # Write data to array and set nodata values + band = dataset.GetRasterBand(1) + band.WriteArray(data) + band.SetNoDataValue(nodata_val) + + # Close file + dataset = None + + +def mostcommon_crs(dc, product, query): + """ + Takes a given query and returns the most common CRS for observations + returned for that spatial extent. This can be useful when your study + area lies on the boundary of two UTM zones, forcing you to decide + which CRS to use for your `output_crs` in `dc.load`. + + Parameters + ---------- + dc : datacube Datacube object + The Datacube to connect to, i.e. `dc = datacube.Datacube()`. + This allows you to also use development datacubes if required. + product : str + A product name to load CRSs from + query : dict + A datacube query including x, y and time range to assess for the + most common CRS + + Returns + ------- + str + A EPSG string giving the most common CRS from all datasets returned + by the query above + + """ + + # remove dask_chunks & align to prevent func failing + # prevent function altering dictionary kwargs + query = deepcopy(query) + if "dask_chunks" in query: + query.pop("dask_chunks", None) + + if "align" in query: + query.pop("align", None) + + # List of matching products + matching_datasets = dc.find_datasets(product=product, **query) + + # Extract all CRSs + crs_list = [str(i.crs) for i in matching_datasets] + + # Identify most common CRS + crs_counts = Counter(crs_list) + crs_mostcommon = crs_counts.most_common(1)[0][0] + + # Warn user if multiple CRSs are encountered + if len(crs_counts.keys()) > 1: + + warnings.warn( + f"Multiple UTM zones {list(crs_counts.keys())} " + f"were returned for this query. Defaulting to " + f"the most common zone: {crs_mostcommon}", + UserWarning, + ) + + return crs_mostcommon + + +def download_unzip(url, output_dir=None, remove_zip=True): + """ + Downloads and unzips a .zip file from an external URL to a local + directory. + + Parameters + ---------- + url : str + A string giving a URL path to the zip file you wish to download + and unzip + output_dir : str, optional + An optional string giving the directory to unzip files into. + Defaults to None, which will unzip files in the current working + directory + remove_zip : bool, optional + An optional boolean indicating whether to remove the downloaded + .zip file after files are unzipped. Defaults to True, which will + delete the .zip file. + + """ + + # Get basename for zip file + zip_name = os.path.basename(url) + + # Raise exception if the file is not of type .zip + if not zip_name.endswith(".zip"): + raise ValueError( + f"The URL provided does not point to a .zip " + f"file (e.g. {zip_name}). Please specify a " + f"URL path to a valid .zip file" + ) + + # Download zip file + print(f"Downloading {zip_name}") + r = requests.get(url) + with open(zip_name, "wb") as f: + f.write(r.content) + + # Extract into output_dir + with zipfile.ZipFile(zip_name, "r") as zip_ref: + zip_ref.extractall(output_dir) + print( + f"Unzipping output files to: " + f"{output_dir if output_dir else os.getcwd()}" + ) + + # Optionally cleanup + if remove_zip: + os.remove(zip_name) + + +def wofs_fuser(dest, src): + """ + Fuse two WOfS water measurements represented as `ndarray` objects. + + Note: this is a copy of the function located here: + https://github.com/GeoscienceAustralia/digitalearthau/blob/develop/digitalearthau/utils.py + """ + empty = (dest & 1).astype(bool) + both = ~empty & ~((src & 1).astype(bool)) + dest[empty] = src[empty] + dest[both] |= src[both] + + +def dilate(array, dilation=10, invert=True): + """ + Dilate a binary array by a specified nummber of pixels using a + disk-like radial dilation. + + By default, invalid (e.g. False or 0) values are dilated. This is + suitable for applications such as cloud masking (e.g. creating a + buffer around cloudy or shadowed pixels). This functionality can + be reversed by specifying `invert=False`. + + Parameters + ---------- + array : array + The binary array to dilate. + dilation : int, optional + An optional integer specifying the number of pixels to dilate + by. Defaults to 10, which will dilate `array` by 10 pixels. + invert : bool, optional + An optional boolean specifying whether to invert the binary + array prior to dilation. The default is True, which dilates the + invalid values in the array (e.g. False or 0 values). + + Returns + ------- + array + An array of the same shape as `array`, with valid data pixels + dilated by the number of pixels specified by `dilation`. + """ + + y, x = np.ogrid[ + -dilation : (dilation + 1), + -dilation : (dilation + 1), + ] + + # disk-like radial dilation + kernel = (x * x) + (y * y) <= (dilation + 0.5) ** 2 + + # If invert=True, invert True values to False etc + if invert: + array = ~array + + return ~binary_dilation( + array.astype(bool), structure=kernel.reshape((1,) + kernel.shape) + ) + + +def _select_along_axis(values, idx, axis): + other_ind = np.ix_(*[np.arange(s) for s in idx.shape]) + sl = other_ind[:axis] + (idx,) + other_ind[axis:] + return values[sl] + + +def first(array: xr.DataArray, dim: str, index_name: str = None) -> xr.DataArray: + """ + Finds the first occuring non-null value along the given dimension. + + Parameters + ---------- + array : xr.DataArray + The array to search. + dim : str + The name of the dimension to reduce by finding the first non-null value. + + Returns + ------- + reduced : xr.DataArray + An array of the first non-null values. + The `dim` dimension will be removed, and replaced with a coord of the + same name, containing the value of that dimension where the last value + was found. + """ + axis = array.get_axis_num(dim) + idx_first = np.argmax(~pd.isnull(array), axis=axis) + reduced = array.reduce(_select_along_axis, idx=idx_first, axis=axis) + reduced[dim] = array[dim].isel({dim: xr.DataArray(idx_first, dims=reduced.dims)}) + if index_name is not None: + reduced[index_name] = xr.DataArray(idx_first, dims=reduced.dims) + return reduced + + +def last(array: xr.DataArray, dim: str, index_name: str = None) -> xr.DataArray: + """ + Finds the last occuring non-null value along the given dimension. + + Parameters + ---------- + array : xr.DataArray + The array to search. + dim : str + The name of the dimension to reduce by finding the last non-null value. + index_name : str, optional + If given, the name of a coordinate to be added containing the index + of where on the dimension the nearest value was found. + + Returns + ------- + reduced : xr.DataArray + An array of the last non-null values. + The `dim` dimension will be removed, and replaced with a coord of the + same name, containing the value of that dimension where the last value + was found. + """ + axis = array.get_axis_num(dim) + rev = (slice(None),) * axis + (slice(None, None, -1),) + idx_last = -1 - np.argmax(~pd.isnull(array)[rev], axis=axis) + reduced = array.reduce(_select_along_axis, idx=idx_last, axis=axis) + reduced[dim] = array[dim].isel({dim: xr.DataArray(idx_last, dims=reduced.dims)}) + if index_name is not None: + reduced[index_name] = xr.DataArray(idx_last, dims=reduced.dims) + return reduced + + +def nearest( + array: xr.DataArray, dim: str, target, index_name: str = None +) -> xr.DataArray: + """ + Finds the nearest values to a target label along the given dimension, for + all other dimensions. + + E.g. For a DataArray with dimensions ('time', 'x', 'y') + + nearest_array = nearest(array, 'time', '2017-03-12') + + will return an array with the dimensions ('x', 'y'), with non-null values + found closest for each (x, y) pixel to that location along the time + dimension. + + The returned array will include the 'time' coordinate for each x,y pixel + that the nearest value was found. + + Parameters + ---------- + array : xr.DataArray + The array to search. + dim : str + The name of the dimension to look for the target label. + target : same type as array[dim] + The value to look up along the given dimension. + index_name : str, optional + If given, the name of a coordinate to be added containing the index + of where on the dimension the nearest value was found. + + Returns + ------- + nearest_array : xr.DataArray + An array of the nearest non-null values to the target label. + The `dim` dimension will be removed, and replaced with a coord of the + same name, containing the value of that dimension closest to the + given target label. + """ + before_target = slice(None, target) + after_target = slice(target, None) + + da_before = array.sel({dim: before_target}) + da_after = array.sel({dim: after_target}) + + da_before = last(da_before, dim, index_name) if da_before[dim].shape[0] else None + da_after = first(da_after, dim, index_name) if da_after[dim].shape[0] else None + + if da_before is None and da_after is not None: + return da_after + if da_after is None and da_before is not None: + return da_before + + target = array[dim].dtype.type(target) + is_before_closer = abs(target - da_before[dim]) < abs(target - da_after[dim]) + nearest_array = xr.where(is_before_closer, da_before, da_after) + nearest_array[dim] = xr.where(is_before_closer, da_before[dim], da_after[dim]) + if index_name is not None: + nearest_array[index_name] = xr.where( + is_before_closer, da_before[index_name], da_after[index_name] + ) + return nearest_array + +def parallel_apply(ds, dim, func, *args): + """ + Applies a custom function in parallel along the dimension of an + xarray.Dataset or xarray.DataArray. + + The function can be any function that can be applied to an + individual xarray.Dataset or xarray.DataArray (e.g. data for a + single timestep). The function should also return data in + xarray.Dataset or xarray.DataArray format. + + This function is useful as a simple method for parallising code + that cannot easily be parallised using Dask. + + Parameters + ---------- + ds : xarray.Dataset or xarray.DataArray + xarray data with a dimension `dim` to apply the custom function + along. + dim : string + The dimension along which the custom function will be applied. + func : function + The function that will be applied in parallel to each array + along dimension `dim`. The first argument passed to this + function should be the array along `dim`. + *args : + Any number of arguments that will be passed to `func`. + + Returns + ------- + xarray.Dataset + A concatenated dataset containing an output for each array + along the input `dim` dimension. + """ + + from concurrent.futures import ProcessPoolExecutor + from tqdm import tqdm + from itertools import repeat + + with ProcessPoolExecutor() as executor: + + # Apply func in parallel + groups = [group for (i, group) in ds.groupby(dim)] + to_iterate = (groups, *(repeat(i, len(groups)) for i in args)) + out_list = list(tqdm(executor.map(func, *to_iterate), total=len(groups))) + + # Combine to match the original dataset + return xr.concat(out_list, dim=ds[dim]) + + +def pan_sharpen_brovey(band_1, band_2, band_3, pan_band): + """ + Brovey pan sharpening on surface reflectance input using numexpr + and return three xarrays. + Parameters + ---------- + band_1, band_2, band_3 : xarray.DataArray or numpy.array + Three input multispectral bands, either as xarray.DataArrays or + numpy.arrays. These bands should have already been resampled to + the spatial resolution of the panchromatic band. + pan_band : xarray.DataArray or numpy.array + A panchromatic band corresponding to the above multispectral + bands that will be used to pan-sharpen the data. + Returns + ------- + band_1_sharpen, band_2_sharpen, band_3_sharpen : numpy.arrays + Three numpy arrays equivelent to `band_1`, `band_2` and `band_3` + pan-sharpened to the spatial resolution of `pan_band`. + """ + # Calculate total + exp = 'band_1 + band_2 + band_3' + total = numexpr.evaluate(exp) + + # Perform Brovey Transform in form of: band/total*panchromatic + exp = 'a/b*c' + band_1_sharpen = numexpr.evaluate(exp, local_dict={'a': band_1, + 'b': total, + 'c': pan_band}) + band_2_sharpen = numexpr.evaluate(exp, local_dict={'a': band_2, + 'b': total, + 'c': pan_band}) + band_3_sharpen = numexpr.evaluate(exp, local_dict={'a': band_3, + 'b': total, + 'c': pan_band}) + + return band_1_sharpen, band_2_sharpen, band_3_sharpen + +def load_s1_by_orbits(dc,query): + ''' + Function to query and load ascending and descending Sentinel-1 data + and add a variable to denote acquisition orbits + + Parameters: + dc: connected datacube + query: a query dictionary to define spatial extent, measurements, time range and spatial resolution + + Returns: + Queried dataset with variable 'is_ascending' added to denote orbit path + + ''' + # load ascending data + print('\nQuerying and loading Sentinel-1 ascending data...') + ds_s1_ascending=load_ard(dc=dc,products=['s1_rtc'],resampling='bilinear', + dtype='native',sat_orbit_state='ascending',**query) + # add an variable denoting data source + ds_s1_ascending['is_ascending']=xr.DataArray(np.ones(len(ds_s1_ascending.time)), + dims=('time'),coords={'time': ds_s1_ascending.time}) + + # load descending data + print('\nQuerying and loading Sentinel-1 descending data...') + ds_s1_descending=load_ard(dc=dc,products=['s1_rtc'],resampling='bilinear', + dtype='native',sat_orbit_state='descending',**query) + # add an variable denoting data source + ds_s1_descending['is_ascending']=xr.DataArray(np.zeros(len(ds_s1_descending.time)), + dims=('time'),coords={'time': ds_s1_descending.time}) + + # merge datasets together + ds_s1=xr.concat([ds_s1_ascending,ds_s1_descending],dim='time').sortby('time') + + return ds_s1 + +def filter_obs_by_orbit(ds_s1): + ''' + Function to impliment per-pixel filtering of Sentinel-1 observations + to keep only observations from the orbit (ascending/descending) with higher frequency over time. + + Each of the Sentinel-1 observations was acquired from either a descending or ascending orbit, + which has impacts on the local incidence angle and backscattering value. + Here we do the filtering to minimise the effects of inconsistent looking angle and obit direction for each individual pixel. + + Parameters: + ds_s1: xarray.Dataset + Time-series observations of Sentinel-1 data, + with two required variables: 'is_ascending' denoting orbit path and 'mask' to identify acquisition exent + + Returns: + ds_s1_filtered: xarray.Dataset + Filtered dataset + ''' + + print('\nFiltering Sentinel-1 product by orbit...') + cnt_ascending=((ds_s1["is_ascending"]==1)&(ds_s1['mask']!=0)).sum(dim='time') + cnt_descending=((ds_s1["is_ascending"]==0)&(ds_s1['mask']!=0)).sum(dim='time') + + ds_s1_filtered=ds_s1.where(((cnt_ascending>=cnt_descending)&(ds_s1["is_ascending"]==1))| + ((cnt_ascending=0 + thresholded_ds = thresholded_ds.where(~nodata) + # use 20% ~ 80% wet frequency to identify potential coastal zone + coastal_mask=(thresholded_ds.mean(dim='time') >= 0.2)&(thresholded_ds.mean(dim='time') <= 0.8) + # buffering + print('\nApplying buffering of {} Sentinel-2 pixels (parameter buffer_pixels)...'.format(buffer_pixels)) + coastal_mask=xr.apply_ufunc(binary_dilation,coastal_mask.compute(),disk(buffer_pixels)) + return coastal_mask + +def choose_product(ds_ls,ds_s2,ds_s1,ds_ls_s2,time_step,**kwargs): + ''' + Rule-based guide on choosing the best availabel dataset in a given time step and optionally within a coastal zone mask + + Parameters: + ds_ls: xarray.Dataset + Time series Landsat data + ds_s2: xarray.Dataset + Time series Sentinel-2 data + ds_s1: xarray.Dataset + Time series Sentinel-1 data + ds_ls_s2: xarray.Dataset or None + Time series of combined Landsat and Sentinel-2 data. + time_step: string + Time step for temporal composition + **kwargs: A set of optional parameters including: + thresh_n_valid: integer + Threhold of minimum average number of valid observations within each time step + thresh_freq: float + Threshold of minimum frequency of valid observations within each time step + buffer_pixels: integer + Number of pixels to buffer coastal zone + coastal_masking: Boolean + whether to calculate a coastal zone mask and restrict the comparison of the products within the mask + Returns: + Xarray.Dataset of the best product + String of the best product name: 'ls', 's2', 's1' or 'ls_s2' + ''' + + # check if optional parameters are defined otherwise set default values + thresh_n_valid=10 if "thresh_n_valid" not in kwargs else kwargs["thresh_n_valid"] + thresh_freq=0.2 if "thresh_freq" not in kwargs else kwargs["thresh_freq"] + buffer_pixels=100 if "buffer_pixels" not in kwargs else kwargs["buffer_pixels"] + print('\nThreshold number of valid observations (parameter thresh_n_valid): {}'.format(thresh_n_valid)) + print('\nThreshold frequency of valid observations (parameter thresh_freq): {}'.format(thresh_freq)) + + # create mask if requested + coastal_masking=False if "coastal_masking" not in kwargs else kwargs["coastal_masking"] + if coastal_masking==True: + # calculate index + ds_s2 = calculate_indices(ds_s2, index='MNDWI', satellite_mission='s2') + mask=create_coastal_mask(ds_s2['MNDWI'],buffer_pixels) + else: + print('\nNo coastal masking required, using all pixels within the selected region...') + mask=None + + # calculate mean number and fraction of clear observations within each timestep and the mask + print('\nCalculating number and frequency of valid observations...') + + n_valid_obs_s2,freq_valid_s2=get_mean_number_freq_valid_obs(ds_s2['green'],mask,time_step) + print('\nSentinel-2: Average number and frequency of valid observations: {:.0f} and {:.2f}'.format(n_valid_obs_s2.mean().values,freq_valid_s2.mean().values)) + + n_valid_obs_ls,freq_valid_ls=get_mean_number_freq_valid_obs(ds_ls['green'],mask,time_step) + print('\nLandsat: Average number and frequency of valid observations: {:.0f} and {:.2f}'.format(n_valid_obs_ls.mean().values,freq_valid_ls.mean().values)) + +# n_valid_obs_s1,freq_valid_s1=get_mean_number_freq_valid_obs(ds_s1['vh'],mask,time_step) # dont need this as sentinel-1 will only be chosen when optical datasets are not sufficient + if not ds_ls_s2 is None: + n_valid_obs_ls_s2,freq_valid_ls_s2=get_mean_number_freq_valid_obs(ds_ls_s2['green'],mask,time_step) + print('\nCombined Landsat and Sentinel-2 product: Average number and frequency of valid observations: {:.0f} and {:.2f}'.format(n_valid_obs_ls_s2.mean().values,freq_valid_ls_s2.mean().values)) + + # apply decision rules + print('\nApplying rules to choose product...') + + # if Sentinel-2 meets requirements + if ((n_valid_obs_s2>=thresh_n_valid).all()) and ((freq_valid_s2>=thresh_freq).all()): + print('\nSentinel-2 product has met the minimum required average number and frequency of valid observations within all time periods') + # if combined product is available, choose combined product if it has both higher number and frequency + if not ds_ls_s2 is None: + if ((n_valid_obs_ls_s2>n_valid_obs_s2).all()) and ((freq_valid_ls_s2>freq_valid_s2).all()): + ds_selected, product_name=ds_ls_s2,'ls_s2' + print('\nChoosing combined Landsat and Sentinel-2 product as it has both higher number and frequency of valid observations within all time periods') + else: + ds_selected, product_name=ds_s2,'s2' + print('\nChoosing Sentinel-2 product as neither Landsat or the combined product meets both requirements or is significantly better than Sentinel-2') + # if combined product is unavailable, choose Landsat if it has both higher number and frequency + elif ((n_valid_obs_ls>=n_valid_obs_s2).all()) and ((freq_valid_ls>=freq_valid_s2).all()): + ds_selected, product_name=ds_ls,'ls' + print('\nChoosing Landsat product as it has both higher average number and frequency of valid observations within all time periods') + # otherwise choose Sentinel-2 + else: + ds_selected, product_name=ds_s2,'s2' + print('\nChoosing Sentinel-2 product as Landsat product does not meet both requirements or is not significantly better than Sentinel-2') + # if Sentinel-2 doesn't meet both requirements,but Landsat does, either choose Landsat or combined product if available + elif ((n_valid_obs_ls>=thresh_n_valid).all()) and ((freq_valid_ls>=thresh_freq).all()): + print('\nSentinel-2 does not meet the minimum required average number and frequency of valid observations within all time periods, but Landsat does') + if not ds_ls_s2 is None: + ds_selected, product_name=ds_ls_s2,'ls_s2' + print('\nChoosing combined Landsat and Sentinel-2 product as it has both higher number and frequency of valid observations within all time periods') + else: + ds_selected, product_name=ds_ls,'ls' + print('\nChoosing Landsat product') + # if neither Sentinel-2 or Landsat meet both requirements, choose combined product if it meets requirements + elif not ds_ls_s2 is None: + print('\nNeither Sentinel-2 or Landsat meets the minimum required average number and frequency of valid observations within all time periods') + # but the combined product meet requirements + if ((n_valid_obs_ls_s2>=thresh_n_valid).all()) and ((freq_valid_ls_s2>=thresh_freq).all()): + ds_selected, product_name=ds_ls_s2,'ls_s2' + print('\nChoosing combined Landsat and Sentinel-2 product as it meets the minimum required average number and frequency of valid observations within all time periods') + else: + ds_selected, product_name=ds_s1,'s1' + print('\nChoosing Sentinel-1 as no other products available that meet the requirements') + # otherwise choose Sentinel-1 + else: + print('\nNeither Sentinel-2 or Landsat meets the minimum required average number and frequency of valid observations within all time periods') + ds_selected, product_name=ds_s1,'s1' + print('\nChoosing Sentinel-1 product as no other products available that meet the requirements') + + print('\nBest available product: ',product_name) + return ds_selected, product_name + +def load_combined_ls_s2(dc,query): + '''function to query and load combined Landsat and Sentinel-2 data + + Parameters: + dc: connected datacube + query: a query dictionary to define spatial extent, time range, measurements and spatial resolution for both datasets + + Returns: + ds_combined: Combined data as xarray.Dataset + ''' + print('Querying and loading combined Landsat and Sentinel-2 products...') + # Load available Landsat data resampled to Sentinel-2 resolution + ds_ls = load_ard(dc=dc, products=['ls8_sr', 'ls9_sr'],align=(10, 10), + resampling='bilinear',**query) + + # add an variable denoting data source (for future analysis) + is_ls=xr.DataArray(np.ones(len(ds_ls.time)),dims=('time'),coords={'time': ds_ls.time}) + ds_ls['is_ls'] = is_ls + + # Load Sentinel-2 data + ds_s2 = load_ard(dc=dc,products=['s2_l2a'],resampling='bilinear', + align=(10, 10),mask_filters=[("opening", 2), ("dilation", 5)],**query) + # add an variable denoting data source (for future analysis) + is_ls=xr.DataArray(np.zeros(len(ds_s2.time)),dims=('time'),coords={'time': ds_s2.time}) + ds_s2['is_ls'] = is_ls + + # merge two datasets together + ds_combined=xr.concat([ds_ls,ds_s2],dim='time').sortby('time') + + return ds_combined + +def load_best_available_ds(dc, lat_range, lon_range, time_range, time_step, **kwargs): + ''' + Function to query, load and compare different products, select and return the best available product + + Parameters: + dc: connected datacube + lat_range: range of latitudes in tuple or list + lon_range: range of longitude in tuple or list + time_range: range of time to query the data in tuple or list + time_step: string, pre-defined time step for temporal aggregation, e.g. '1Y' + **kwargs: A set of optional parameters on data query or comparison between products which may include: + combine_ls_s2: A boolean value indicating whether to include merged/stacked Landsat and Sentinel-2 products as an option. Default to False. + set_resolution: integer of spatial resolution in metres to query all products + coastal_masking: A boolean value indicating whether to calculate a mask + and restrict the comparison of the products within the masked zone. + set_product: Set this to only query and load a pre-selected product, 'ls','s2','ls_s2' or 's1' + i.e. no other products will be queried or compared. + thresh_n_valid: Threhold of minimum average number of valid observations within each time step, integer + thresh_freq: Threshold of minimum frequency of valid observations within each time step, float between 0~1 + buffer_pixels: Number of pixels to buffer coastal zone, integer + + Returns: + ds_selected: selected product as xarray.Dataset + product_name: name of selected product in string format, i.e. 'ls','s2','ls_s2','s1' + ''' + # parse input time range to accommodate queries before and after 2017 + min_time=min(parser.parse(time_range_i,default=datetime(1987,1,1,0,0)) + for time_range_i in time_range) + if min_time ds.lat.max(): +# warnings.warn( +# "Lats must be in range {} .. {}. Got: {}".format( +# ds.lat.min().values, ds.lat.max().values, lat +# ) +# ) +# if min(lon) < ds.lon.min() or max(lon) > ds.lon.max(): +# warnings.warn( +# "Lons must be in range {} .. {}. Got: {}".format( +# ds.lon.min().values, ds.lon.max().values, lon +# ) +# ) +# # Find existing coords between min&max +# lats = ds.lat[np.logical_and(ds.lat >= min(lat), ds.lat <= max(lat))].values +# # If there was nothing between, just plan to grab closest +# if len(lats) == 0: +# lats = np.unique(ds.lat.sel(lat=np.array(lat), method="nearest")) +# lons = ds.lon[np.logical_and(ds.lon >= min(lon), ds.lon <= max(lon))].values +# if len(lons) == 0: +# lons = np.unique(ds.lon.sel(lon=np.array(lon), method="nearest")) +# # crop and keep attrs +# output = ds.sel(lat=lats, lon=lons) +# output.attrs = ds.attrs +# for var in output.data_vars: +# output[var].attrs = ds[var].attrs +# return output + + +# def era5_area_nearest(ds, lat, lon): +# """ +# Crop a dataset containing EAR5 variables to a location. +# The output spatial grid is snapped to the nearest input grid points. + +# Parameters +# ---------- +# ds : xarray dataset +# A dataset containing ERA5 variables of interest. + +# lat: tuple or list +# Latitude range for query. + +# lon: tuple or list +# Longitude range for query. + +# Returns +# ------- +# An xarray dataset containing ERA5 variables for the selected location. + +# """ + +# if min(lon) < 0: +# # re-order along longitude to go from -180 to 180 +# ds = ds.assign_coords({"lon": (((ds.lon + 180) % 360) - 180)}) +# ds = ds.reindex({"lon": np.sort(ds.lon)}) + +# # find the nearest lat lon boundary points +# test = ds.sel(lat=lat, lon=lon, method="nearest") +# # define the lat/lon grid +# lat_range = slice(test.lat.max().values, test.lat.min().values) +# lon_range = slice(test.lon.min().values, test.lon.max().values) +# # crop and keep attrs +# output = ds.sel(lat=lat_range, lon=lon_range) +# output.attrs = ds.attrs +# for var in output.data_vars: +# output[var].attrs = ds[var].attrs +# return output + + +# def load_era5_netcdf(var, lat, lon, time, grid="nearest", **kwargs): +# """ +# Returns a ERA5 variable for a selected location and time window. + +# Parameters +# ---------- +# var : string +# Name of the ERA5 climate variable to download, e.g "air_temperature_at_2_metres" + +# lat: tuple or list +# Latitude range for query. + +# lon: tuple or list +# Longitude range for query. + +# time: tuple or list +# Time range for query. + +# grid: string +# Option for output spatial gridding. +# The default is 'nearest', for which output spatial grid is snapped to the nearest ERA5 input grid points. +# Alternatively, output spatial grid will either include input grid points within lat/lon boundaries or the nearest point if none is within the search location. + +# Returns +# ------- +# An xarray dataset containing the variable for the selected location and time window. + +# """ + +# ds = get_era5_daily(var, time[0], time[1], **kwargs) +# if grid == "nearest": +# return era5_area_nearest(ds, lat, lon).compute() +# else: +# return era5_area_crop(ds, lat, lon).compute() diff --git a/deafrica_tools/load_isda.py b/deafrica_tools/load_isda.py new file mode 100644 index 0000000..2e9cca1 --- /dev/null +++ b/deafrica_tools/load_isda.py @@ -0,0 +1,92 @@ +""" +Functions to retrieve iSDAsoil data. +""" + +import numpy as np +import pandas as pd +import xarray as xr +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +import rasterio as rio +from pyproj import Transformer +import matplotlib.pyplot as plt +import os +import numpy as np + +from urllib.parse import urlparse +import boto3 +from pystac import stac_io, Catalog + +#this function allows us to directly query the data on s3, adapted from iSDA tutorial https://github.com/iSDA-Africa/isdasoil-tutorial/blob/main/iSDAsoil-tutorial.ipynb +def my_read_method(uri): + parsed = urlparse(uri) + if parsed.scheme == 's3': + bucket = parsed.netloc + key = parsed.path[1:] + s3 = boto3.resource('s3') + obj = s3.Object(bucket, key) + return obj.get()['Body'].read().decode('utf-8') + else: + return stac_io.default_read_text_method(uri) + +stac_io.read_text_method = my_read_method + +catalog = Catalog.from_file("https://isdasoil.s3.amazonaws.com/catalog.json") + +assets = {} + +for root, catalogs, items in catalog.walk(): + for item in items: + str(f"Type: {item.get_parent().title}") + # save all items to a dictionary as we go along + assets[item.id] = item + for asset in item.assets.values(): + if asset.roles == ['data']: + str(f"Title: {asset.title}") + str(f"Description: {asset.description}") + str(f"URL: {asset.href}") + str("------------") + +# define load_isda() function + +def load_isda(var, lat, lon): + """ + Download and return iSDA variable with number of bands corresponding to number of iSDA layers. + Parameters + ---------- + var : string + Name of the iSDA variable to download, e.g "ph" + lat: tuple or list + Latitude range for query. + lon: tuple or list + Longitude range for query. + """ + + bands = assets[var].assets["image"].extra_fields.get('eo:bands') + bands = [val['description'] for val in bands] + + if len(np.unique(bands)) > 1: + + ds = xr.open_dataset(assets[var].assets["image"].href, engine="rasterio").rio.clip_box( + minx=lon[0], + miny=lat[0], + maxx=lon[1], + maxy=lat[1], + crs="EPSG:4326", + ) + + ds_layered = ds.drop_dims('band') + for x in np.unique(ds.band): + ds_layered[bands[x-1]] = ds.sel(band=x).to_array(dim='band').squeeze() + + else: + + ds_layered = xr.open_dataset(assets[var].assets["image"].href, engine="rasterio").rio.clip_box( + minx=lon[0], + miny=lat[0], + maxx=lon[1], + maxy=lat[1], + crs="EPSG:4326", + ).squeeze() + + return ds_layered diff --git a/deafrica_tools/load_soil_moisture.py b/deafrica_tools/load_soil_moisture.py new file mode 100644 index 0000000..4fb1a58 --- /dev/null +++ b/deafrica_tools/load_soil_moisture.py @@ -0,0 +1,39 @@ +import xarray as xr +import numpy as np + +# function to load soil moisture data + +def load_soil_moisture(lat, lon, time, product = 'surface', grid = 'nearest'): + product_baseurl = 'https://dapds00.nci.org.au/thredds/dodsC/ub8/global/GRAFS/' + assert product in ['surface', 'rootzone'], 'product parameter must be surface or root-zone' + # lat, lon grid + if grid == 'nearest': + # select lat/lon range from data; snap to nearest grid + lat_range, lon_range = None, None + else: + # define a grid that covers the entire area of interest + lat_range = np.arange(np.max(np.ceil(np.array(lat)*10.+0.5)/10.-0.05), np.min(np.floor(np.array(lat)*10.-0.5)/10.+0.05)-0.05, -0.1) + lon_range = np.arange(np.min(np.floor(np.array(lon)*10.-0.5)/10.+0.05), np.max(np.ceil(np.array(lon)*10.+0.5)/10.-0.05)+0.05, 0.1) + # split time window into years + day_range = np.array(time).astype("M8[D]") + year_range = np.array(time).astype("M8[Y]") + if product == 'surface': + product_name = 'GRAFS_TopSoilRelativeWetness_' + else: product_name = 'GRAFS_RootzoneSoilWaterIndex_' + datasets = [] + for year in np.arange(year_range[0], year_range[1]+1, np.timedelta64(1, 'Y')): + start = np.max([day_range[0], year.astype("M8[D]")]) + end = np.min([day_range[1], (year+1).astype("M8[D]")-1]) + product_url = product_baseurl + product_name +'%s.nc'%str(year) + print(product_url) + # data is loaded lazily through OPeNDAP + ds = xr.open_dataset(product_url) + if lat_range is None: + # select lat/lon range from data if not specified; snap to nearest grid + test = ds.sel(lat=list(lat), lon=list(lon), method='nearest') + lat_range = slice(test.lat.values[0], test.lat.values[1]) + lon_range = slice(test.lon.values[0], test.lon.values[1]) + # slice before return + ds = ds.sel(lat=lat_range, lon=lon_range, time=slice(start, end)).compute() + datasets.append(ds) + return xr.merge(datasets) \ No newline at end of file diff --git a/deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.mo b/deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.mo new file mode 100644 index 0000000000000000000000000000000000000000..6b7f6ac439a5e06490962760ae72a8482a9e87b3 GIT binary patch literal 1783 zcmZvcO>Y}T7{{kj-V8{nKve`t59%QjiktFsNEdMk!HFy-IEqcu9#A#jeQXb!omu9^ zsrwO-dO-o>#vx%!E=uZaRFQdp8-Drp96Oq-e|s$z!$MT1J8qZz-PfPz_kA> za1Hzpd>VWJz6|~jJ`Vl`z5@OWz64&vVmjXjll^O8Z5MnU^BZ6~e*`AK?to8$cboaw zU<>m(h)4W@;T!NixC(v(C$E8@gBQV{!RNsTV2a}p@DlhBm~wUTi6!3w@rXSP$)5u- z`Im$1;1o>peGgs+?}N#|hv2*5BQVAH>XSme0A2-CJR4xT? zr-r|P>E1^m9`R=yHXKVDNv%B{sW4H79Ot$y*%VxmR;3EdJ<3zCmb7$ZUZqO2^h~iT zWMG=v=%})KHnv7|`uklvv`!b)E#;WqOms{^A7W!Bc4bAqDLXA^XC_|kDx=C+(zdnr zW|@hP?D3`KS#ut^z?xyvi=m2vHv0$J@%x^(ArpJ-ay9{0Y0JqxC~KsqO$&>wbjZQ5 z_p2n~&LP}!V$a#J!`1zelA^%mRftRl zOhRQ#>2ae*k{sF|gm?uyCav4%aPaSJHS+x-&4iQ0(8f7PAt2BwFLjG}@ z1F?)tO)u_wv4v#RO0Gl1WHCpXp;@irNa~SHbZ%iWrdH^m`pAwn!l)&`xQ9IYbT?{J zSE^W12Ik!2+wlJ_aGX?>Y=|yxayU{s!VRe<;7>eBPTdkRpYSwQ_)Ie(Y`ubzke4dr z{|_cUwMP4}H|nRxSDK0yi58zhJ_X!N4*F$EP$F+NnJ94acrg!khe2mBb#^J`)r(|D LtD$32XOzW%y9ov3 literal 0 HcmV?d00001 diff --git a/deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.po b/deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.po new file mode 100644 index 0000000..06d9e91 --- /dev/null +++ b/deafrica_tools/locales/fr/LC_MESSAGES/deafrica_tools.po @@ -0,0 +1,117 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: deafrica_tools\n" +"Language: fr\n" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:83 +msgid "None" +msgstr "Aucun" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:84 +msgid "ESRI World Imagery" +msgstr "Imagerie mondiale ESRI" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:85 +msgid "Sentinel-2 Geomedian" +msgstr "Sentinel-2 Geomedian" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:86 +msgid "Water Observations from Space" +msgstr "Observations de l'eau depuis l'espace-WOfS" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:99 +msgid "Wetlands Insight Tool" +msgstr "Outil d'analyse des zones humides" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:100 +msgid "Select parameters and AOI" +msgstr "Sélectionner les paramètres et la zone d'intérêt" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:124 +msgid "Total polygon area" +msgstr "Superficie totale du polygone" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:128 +msgid "Area falls within recommended limit" +msgstr "La zone se situe dans la limite recommandée" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:131 +msgid "Area is too large, please update your polygon" +msgstr "La zone est trop grande, veuillez réduire votre polygone" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:151 +msgid "Map Overlays" +msgstr "Superpositions de cartes" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:176 +msgid "Run" +msgstr "Exécuter" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:183 +msgid "Map Overlay:" +msgstr "Carte superposée :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:185 +msgid "Start Date:" +msgstr "Date de début :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:187 +msgid "End Date:" +msgstr "Date de fin :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:189 +msgid "Minimum Good Data:" +msgstr "Minimum de bonnes données :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:191 +msgid "Resampling Frequency:" +msgstr "Fréquence de rééchantillonnage :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:193 +msgid "Output CSV:" +msgstr "Sortie CSV :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:195 +msgid "Output Plot:" +msgstr "Tracé de sortie :" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:308 +msgid "Progress" +msgstr "Progrès" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:326 +msgid "WIT complete" +msgstr "WIT achevée" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:328 +msgid "No polygon selected" +msgstr "Aucun polygone sélectionné" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:365 +msgid "open water" +msgstr "eau libre" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:366 +msgid "wet" +msgstr "humide" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:367 +msgid "green veg" +msgstr "végétation verts" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:368 +msgid "dry veg" +msgstr "végétation seche" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:369 +msgid "bare soil" +msgstr "sol nu" + +#: Tools/deafrica_tools/app/wetlandsinsighttool.py:382 +msgid "Percentage Fractional Cover, Wetness, and Water" +msgstr "Pourcentage de couverture fractionnée, humidité et eau" + diff --git a/deafrica_tools/plotting.py b/deafrica_tools/plotting.py new file mode 100644 index 0000000..d554a4c --- /dev/null +++ b/deafrica_tools/plotting.py @@ -0,0 +1,1272 @@ +""" +Functions for plotting Digital Earth Africa data. +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import math +import folium +import ipywidgets +import branca +import numpy as np +import geopandas as gpd +import matplotlib as mpl +import matplotlib.patheffects as PathEffects +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from datetime import datetime +import matplotlib.cm as cm +from matplotlib import colors as mcolours +from pyproj import Transformer +from IPython.display import display +from matplotlib.colors import ListedColormap +from mpl_toolkits.axes_grid1.inset_locator import inset_axes +from mpl_toolkits.axes_grid1 import make_axes_locatable +from ipyleaflet import Map, Marker, Popup, GeoJSON, basemaps, Choropleth +from skimage import exposure +from branca.colormap import linear +from odc.ui import image_aspect + +from matplotlib.animation import FuncAnimation +import pandas as pd +from pathlib import Path +from shapely.geometry import box +from skimage.exposure import rescale_intensity +from tqdm.auto import tqdm +import warnings + + +def rgb( + ds, + bands=["red", "green", "blue"], + index=None, + index_dim="time", + robust=True, + percentile_stretch=None, + col_wrap=4, + size=6, + aspect=None, + savefig_path=None, + savefig_kwargs={}, + **kwargs, +): + + """ + Takes an xarray dataset and plots RGB images using three imagery + bands (e.g ['red', 'green', 'blue']). The `index` + parameter allows easily selecting individual or multiple images for + RGB plotting. Images can be saved to file by specifying an output + path using `savefig_path`. + This function was designed to work as an easier-to-use wrapper + around xarray's `.plot.imshow()` functionality. + + Last modified: April 2021 + + Parameters + ---------- + ds : xarray Dataset + A two-dimensional or multi-dimensional array to plot as an RGB + image. If the array has more than two dimensions (e.g. multiple + observations along a 'time' dimension), either use `index` to + select one (`index=0`) or multiple observations + (`index=[0, 1]`), or create a custom faceted plot using e.g. + `col="time"`. + bands : list of strings, optional + A list of three strings giving the band names to plot. Defaults + to '['red', 'green', 'blue']'. If the dataset does not contain + bands named `'red', 'green', 'blue'`, then `bands` must be + specified. + index : integer or list of integers, optional + `index` can be used to select one (`index=0`) or multiple + observations (`index=[0, 1]`) from the input dataset for + plotting. If multiple images are requested these will be plotted + as a faceted plot. + index_dim : string, optional + The dimension along which observations should be plotted if + multiple observations are requested using `index`. Defaults to + `time`. + robust : bool, optional + Produces an enhanced image where the colormap range is computed + with 2nd and 98th percentiles instead of the extreme values. + Defaults to True. + percentile_stretch : tuple of floats + An tuple of two floats (between 0.00 and 1.00) that can be used + to clip the colormap range to manually specified percentiles to + get more control over the brightness and contrast of the image. + The default is None; '(0.02, 0.98)' is equivelent to + `robust=True`. If this parameter is used, `robust` will have no + effect. + col_wrap : integer, optional + The number of columns allowed in faceted plots. Defaults to 4. + size : integer, optional + The height (in inches) of each plot. Defaults to 6. + aspect : integer, optional + Aspect ratio of each facet in the plot, so that aspect * size + gives width of each facet in inches. Defaults to None, which + will calculate the aspect based on the x and y dimensions of + the input data. + savefig_path : string, optional + Path to export image file for the RGB plot. Defaults to None, + which does not export an image file. + savefig_kwargs : dict, optional + A dict of keyword arguments to pass to + `matplotlib.pyplot.savefig` when exporting an image file. For + all available options, see: + https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html + **kwargs : optional + Additional keyword arguments to pass to `xarray.plot.imshow()`. + For example, the function can be used to plot into an existing + matplotlib axes object by passing an `ax` keyword argument. + For more options, see: + http://xarray.pydata.org/en/stable/generated/xarray.plot.imshow.html + Returns + ------- + An RGB plot of one or multiple observations, and optionally an image + file written to file. + """ + + # If bands are not in the dataset + ds_vars = list(ds.data_vars) + if set(bands).issubset(ds_vars) == False: + raise ValueError( + "rgb() bands do not match band names in dataset. " + "Note the default rgb() bands are ['red', 'green', 'blue']." + ) + + # If ax is supplied via kwargs, ignore aspect and size + if "ax" in kwargs: + + # Create empty aspect size kwarg that will be passed to imshow + aspect_size_kwarg = {} + else: + # Compute image aspect + if not aspect: + aspect = image_aspect(ds) + + # Populate aspect size kwarg with aspect and size data + aspect_size_kwarg = {"aspect": aspect, "size": size} + + # If no value is supplied for `index` (the default), plot using default + # values and arguments passed via `**kwargs` + if index is None: + + # Select bands and convert to DataArray + da = ds[bands].to_array() + + # If percentile_stretch == True, clip plotting to percentile vmin, vmax + if percentile_stretch: + vmin, vmax = da.compute().quantile(percentile_stretch).values + kwargs.update({"vmin": vmin, "vmax": vmax}) + + # If there are more than three dimensions and the index dimension == 1, + # squeeze this dimension out to remove it + if (len(ds.dims) > 2) and ("col" not in kwargs) and (len(da[index_dim]) == 1): + + da = da.squeeze(dim=index_dim) + + # If there are more than three dimensions and the index dimension + # is longer than 1, raise exception to tell user to use 'col'/`index` + elif (len(ds.dims) > 2) and ("col" not in kwargs) and (len(da[index_dim]) > 1): + + raise Exception( + f"The input dataset `ds` has more than two dimensions: " + "{list(ds.dims.keys())}. Please select a single observation " + "using e.g. `index=0`, or enable faceted plotting by adding " + 'the arguments e.g. `col="time", col_wrap=4` to the function ' + "call" + ) + da = da.compute() + img = da.plot.imshow( + robust=robust, col_wrap=col_wrap, **aspect_size_kwarg, **kwargs + ) + + # If values provided for `index`, extract corresponding observations and + # plot as either single image or facet plot + else: + + # If a float is supplied instead of an integer index, raise exception + if isinstance(index, float): + raise Exception( + f"Please supply `index` as either an integer or a list of " "integers" + ) + + # If col argument is supplied as well as `index`, raise exception + if "col" in kwargs: + raise Exception( + f"Cannot supply both `index` and `col`; please remove one and " + "try again" + ) + + # Convert index to generic type list so that number of indices supplied + # can be computed + index = index if isinstance(index, list) else [index] + + # Select bands and observations and convert to DataArray + da = ds[bands].isel(**{index_dim: index}).to_array().compute() + + # If percentile_stretch == True, clip plotting to percentile vmin, vmax + if percentile_stretch: + vmin, vmax = da.compute().quantile(percentile_stretch).values + kwargs.update({"vmin": vmin, "vmax": vmax}) + + # If multiple index values are supplied, plot as a faceted plot + if len(index) > 1: + + img = da.plot.imshow( + robust=robust, + col=index_dim, + col_wrap=col_wrap, + **aspect_size_kwarg, + **kwargs, + ) + + # If only one index is supplied, squeeze out index_dim and plot as a + # single panel + else: + + img = da.squeeze(dim=index_dim).plot.imshow( + robust=robust, **aspect_size_kwarg, **kwargs + ) + + # If an export path is provided, save image to file. Individual and + # faceted plots have a different API (figure vs fig) so we get around this + # using a try statement: + if savefig_path: + + print(f"Exporting image to {savefig_path}") + + try: + img.fig.savefig(savefig_path, **savefig_kwargs) + except: + img.figure.savefig(savefig_path, **savefig_kwargs) + + +def display_map(x, y, crs="EPSG:4326", margin=-0.5, zoom_bias=0): + """ + Given a set of x and y coordinates, this function generates an + interactive map with a bounded rectangle overlayed on Google Maps + imagery. + + Last modified: September 2019 + + Modified from function written by Otto Wagner available here: + https://github.com/ceos-seo/data_cube_utilities/tree/master/data_cube_utilities + + Parameters + ---------- + x : (float, float) + A tuple of x coordinates in (min, max) format. + y : (float, float) + A tuple of y coordinates in (min, max) format. + crs : string, optional + A string giving the EPSG CRS code of the supplied coordinates. + The default is 'EPSG:4326'. + margin : float + A numeric value giving the number of degrees lat-long to pad + the edges of the rectangular overlay polygon. A larger value + results more space between the edge of the plot and the sides + of the polygon. Defaults to -0.5. + zoom_bias : float or int + A numeric value allowing you to increase or decrease the zoom + level by one step. Defaults to 0; set to greater than 0 to zoom + in, and less than 0 to zoom out. + Returns + ------- + folium.Map : A map centered on the supplied coordinate bounds. A + rectangle is drawn on this map detailing the perimeter of the x, y + bounds. A zoom level is calculated such that the resulting + viewport is the closest it can possibly get to the centered + bounding rectangle without clipping it. + """ + + # Convert each corner coordinates to lat-lon + all_x = (x[0], x[1], x[0], x[1]) + all_y = (y[0], y[0], y[1], y[1]) + transformer = Transformer.from_crs(crs, "EPSG:4326") + all_longitude, all_latitude = transformer.transform(all_x, all_y) + + # Calculate zoom level based on coordinates + lat_zoom_level = ( + _degree_to_zoom_level(min(all_latitude), max(all_latitude), margin=margin) + + zoom_bias + ) + lon_zoom_level = ( + _degree_to_zoom_level(min(all_longitude), max(all_longitude), margin=margin) + + zoom_bias + ) + zoom_level = min(lat_zoom_level, lon_zoom_level) + + # Identify centre point for plotting + center = [np.mean(all_latitude), np.mean(all_longitude)] + + # Create map + interactive_map = folium.Map( + location=center, + zoom_start=zoom_level, + tiles="http://mt1.google.com/vt/lyrs=y&z={z}&x={x}&y={y}", + attr="Google", + ) + + # Create bounding box coordinates to overlay on map + line_segments = [ + (all_latitude[0], all_longitude[0]), + (all_latitude[1], all_longitude[1]), + (all_latitude[3], all_longitude[3]), + (all_latitude[2], all_longitude[2]), + (all_latitude[0], all_longitude[0]), + ] + + # Add bounding box as an overlay + interactive_map.add_child( + folium.features.PolyLine(locations=line_segments, color="red", opacity=0.8) + ) + + # Add clickable lat-lon popup box + interactive_map.add_child(folium.features.LatLngPopup()) + + return interactive_map + + +def map_shapefile( + gdf, + attribute, + continuous=False, + cmap="viridis", + basemap=basemaps.Esri.WorldImagery, + default_zoom=None, + hover_col=True, + **style_kwargs, +): + """ + Plots a geopandas GeoDataFrame over an interactive ipyleaflet + basemap, with features coloured based on attribute column values. + Optionally, can be set up to print selected data from features in + the GeoDataFrame. + + Last modified: February 2020 + + Parameters + ---------- + gdf : geopandas.GeoDataFrame + A GeoDataFrame containing the spatial features to be plotted + over the basemap. + attribute: string, required + An required string giving the name of any column in the + GeoDataFrame you wish to have coloured on the choropleth. + continuous: boolean, optional + Whether to plot data as a categorical or continuous variable. + Defaults to remapping the attribute which is suitable for + categorical data. For continuous data set `continuous` to True. + cmap : string, optional + A string giving the name of a `matplotlib.cm` colormap that will + be used to style the features in the GeoDataFrame. Features will + be coloured based on the selected attribute. Defaults to the + 'viridis' colormap. + basemap : ipyleaflet.basemaps object, optional + An optional `ipyleaflet.basemaps` object used as the basemap for + the interactive plot. Defaults to `basemaps.Esri.WorldImagery`. + default_zoom : int, optional + An optional integer giving a default zoom level for the + interactive ipyleaflet plot. Defaults to None, which infers + the zoom level from the extent of the data. + hover_col : boolean or str, optional + If True (the default), the function will print values from the + GeoDataFrame's `attribute` column above the interactive map when + a user hovers over the features in the map. Alternatively, a + custom shapefile field can be specified by supplying a string + giving the name of the field to print. Set to False to prevent + any attributes from being printed. + **style_kwargs : + Optional keyword arguments to pass to the `style` paramemter of + the `ipyleaflet.Choropleth` function. This can be used to + control the appearance of the shapefile, for example 'stroke' + and 'weight' (controlling line width), 'fillOpacity' (polygon + transparency) and 'dashArray' (whether to plot lines/outlines + with dashes). For more information: + https://ipyleaflet.readthedocs.io/en/latest/api_reference/choropleth.html + """ + + def on_hover(event, id, properties): + with dbg: + text = properties.get(hover_col, "???") + lbl.value = f"{hover_col}: {text}" + + # Verify that attribute exists in shapefile + if attribute not in gdf.columns: + raise ValueError( + f"The `attribute` {attribute} does not exist " + f"in the geopandas.GeoDataFrame. " + f"Valid attributes include {gdf.columns.values}." + ) + + # If hover_col is True, use 'attribute' as the default hover attribute. + # Otherwise, hover_col will use the supplied attribute field name + if hover_col and (hover_col is True): + hover_col = attribute + + # If a custom string if supplied to hover_col, check this exists + elif hover_col and (type(hover_col) == str): + if hover_col not in gdf.columns: + raise ValueError( + f"The `hover_col` field {hover_col} does " + f"not exist in the geopandas.GeoDataFrame. " + f"Valid attributes include " + f"{gdf.columns.values}." + ) + + # Convert to WGS 84 and GeoJSON format + gdf_wgs84 = gdf.to_crs(epsg=4326) + data_geojson = gdf_wgs84.__geo_interface__ + + # If continuous is False, remap categorical classes for visualisation + if not continuous: + + # Zip classes data together to make a dictionary + classes_uni = list(gdf[attribute].unique()) + classes_clean = list(range(0, len(classes_uni))) + classes_dict = dict(zip(classes_uni, classes_clean)) + + # Get values to colour by as a list + classes = gdf[attribute].map(classes_dict).tolist() + + # If continuous is True then do not remap + else: + + # Get values to colour by as a list + classes = gdf[attribute].tolist() + + # Create the dictionary to colour map by + keys = gdf.index + id_class_dict = dict(zip(keys.astype(str), classes)) + + # Get centroid to focus map on + lon1, lat1, lon2, lat2 = gdf_wgs84.total_bounds + lon = (lon1 + lon2) / 2 + lat = (lat1 + lat2) / 2 + + if default_zoom is None: + + # Calculate default zoom from latitude of features + default_zoom = _degree_to_zoom_level(lat1, lat2, margin=-0.5) + + # Plot map + m = Map( + center=(lat, lon), + zoom=default_zoom, + basemap=basemap, + layout=dict(width="800px", height="600px"), + ) + + # Define default plotting parameters for the choropleth map. + # The nested dict structure sets default values which can be + # overwritten/customised by `choropleth_kwargs` values + style_kwargs = dict({"fillOpacity": 0.8}, **style_kwargs) + + # Get `branca.colormap` object from matplotlib string + cm_cmap = cm.get_cmap(cmap, 30) + colormap = branca.colormap.LinearColormap( + [cm_cmap(i) for i in np.linspace(0, 1, 30)] + ) + + # Create the choropleth + choropleth = Choropleth( + geo_data=data_geojson, + choro_data=id_class_dict, + colormap=colormap, + style=style_kwargs, + ) + + # If the vector data contains line features, they will not be + # be coloured by default. To resolve this, we need to manually copy + # across the 'fillColor' attribute to the 'color' attribute for each + # feature, then plot the data as a GeoJSON layer rather than the + # choropleth layer that we use for polygon data. + linefeatures = any( + x in ["LineString", "MultiLineString"] for x in gdf.geometry.type.values + ) + if linefeatures: + + # Copy colour from fill to line edge colour + for i in keys: + choropleth.data["features"][i]["properties"]["style"][ + "color" + ] = choropleth.data["features"][i]["properties"]["style"]["fillColor"] + + # Add GeoJSON layer to map + feature_layer = GeoJSON(data=choropleth.data, style=style_kwargs) + m.add_layer(feature_layer) + + else: + + # Add Choropleth layer to map + m.add_layer(choropleth) + + # If a column is specified by `hover_col`, print data from the + # hovered feature above the map + if hover_col and not linefeatures: + + # Use cholopleth object if data is polygon + lbl = ipywidgets.Label() + dbg = ipywidgets.Output() + choropleth.on_hover(on_hover) + display(lbl) + + else: + + lbl = ipywidgets.Label() + dbg = ipywidgets.Output() + feature_layer.on_hover(on_hover) + display(lbl) + + # Display the map + display(m) + + +def xr_animation( + ds, + bands=None, + output_path="animation.mp4", + width_pixels=500, + interval=100, + percentile_stretch=(0.02, 0.98), + image_proc_funcs=None, + show_gdf=None, + show_date="%d %b %Y", + show_text=None, + show_colorbar=True, + gdf_kwargs={}, + annotation_kwargs={}, + imshow_kwargs={}, + colorbar_kwargs={}, + limit=None, +): + """ + Takes an `xarray` timeseries and animates the data as either a + three-band (e.g. true or false colour) or single-band animation, + allowing changes in the landscape to be compared across time. + + Animations can be customised to include text and date annotations + or use specific combinations of input bands. Vector data can be + overlaid and animated on top of imagery, and custom image + processing functions can be applied to each frame. + Supports .mp4 (ideal for Twitter/social media) and .gif (ideal + for all purposes, but can have large file sizes) format files. + + Last modified: October 2020 + + Parameters + ---------- + ds : xarray.Dataset + An xarray dataset with multiple time steps (i.e. multiple + observations along the `time` dimension). + bands : list of strings + An list of either one or three band names to be plotted, + all of which must exist in `ds`. + output_path : str, optional + A string giving the output location and filename of the + resulting animation. File extensions of '.mp4' and '.gif' are + accepted. Defaults to 'animation.mp4'. + width_pixels : int, optional + An integer defining the output width in pixels for the + resulting animation. The height of the animation is set + automatically based on the dimensions/ratio of the input + xarray dataset. Defaults to 500 pixels wide. + interval : int, optional + An integer defining the milliseconds between each animation + frame used to control the speed of the output animation. Higher + values result in a slower animation. Defaults to 100 + milliseconds between each frame. + percentile_stretch : tuple of floats, optional + An optional tuple of two floats that can be used to clip one or + three-band arrays by percentiles to produce a more vibrant, + visually attractive image that is not affected by outliers/ + extreme values. The default is `(0.02, 0.98)` which is + equivalent to xarray's `robust=True`. This parameter is ignored + completely if `vmin` and `vmax` are provided as kwargs to + `imshow_kwargs`. + image_proc_funcs : list of funcs, optional + An optional list containing functions that will be applied to + each animation frame (timestep) prior to animating. This can + include image processing functions such as increasing contrast, + unsharp masking, saturation etc. The function should take AND + return a `numpy.ndarray` with shape [y, x, bands]. If your + function has parameters, you can pass in custom values using + a lambda function: + `image_proc_funcs=[lambda x: custom_func(x, param1=10)]`. + show_gdf: geopandas.GeoDataFrame, optional + Vector data (e.g. ESRI shapefiles or GeoJSON) can be optionally + plotted over the top of imagery by supplying a + `geopandas.GeoDataFrame` object. To customise colours used to + plot the vector features, create a new column in the + GeoDataFrame called 'colors' specifying the colour used to plot + each feature: e.g. `gdf['colors'] = 'red'`. + To plot vector features at specific moments in time during the + animation, create new 'start_time' and/or 'end_time' columns in + the GeoDataFrame that define the time range used to plot each + feature. Dates can be provided in any string format that can be + converted using the `pandas.to_datetime()`. e.g. + `gdf['end_time'] = ['2001', '2005-01', '2009-01-01']` + show_date : string or bool, optional + An optional string or bool that defines how (or if) to plot + date annotations for each animation frame. Defaults to + '%d %b %Y'; can be customised to any format understood by + strftime (https://strftime.org/). Set to False to remove date + annotations completely. + show_text : str or list of strings, optional + An optional string or list of strings with a length equal to + the number of timesteps in `ds`. This can be used to display a + static text annotation (using a string), or a dynamic title + (using a list) that displays different text for each timestep. + By default, no text annotation will be plotted. + show_colorbar : bool, optional + An optional boolean indicating whether to include a colourbar + for single-band animations. Defaults to True. + gdf_kwargs : dict, optional + An optional dictionary of keyword arguments to customise the + appearance of a `geopandas.GeoDataFrame` supplied to + `show_gdf`. Keyword arguments are passed to `GeoSeries.plot` + (see http://geopandas.org/reference.html#geopandas.GeoSeries.plot). + For example: `gdf_kwargs = {'linewidth': 2}`. + annotation_kwargs : dict, optional + An optional dict of keyword arguments for controlling the + appearance of text annotations. Keyword arguments are passed + to `plt.annotate` from `matplotlib`. + (see https://matplotlib.org/api/_as_gen/matplotlib.pyplot.annotate.html + for options). For example, + `annotation_kwargs={'fontsize':20, 'color':'red', 'family':'serif'}`. + imshow_kwargs : dict, optional + An optional dict of keyword arguments for controlling the + appearance of arrays passed to `matplotlib`'s `plt.imshow` + (see https://matplotlib.org/api/_as_gen/matplotlib.pyplot.imshow.html + for options). For example, a green colour scheme and custom + stretch could be specified using: + `onebandplot_kwargs={'cmap':'Greens`, 'vmin':0.2, 'vmax':0.9}`. + (some parameters like 'cmap' will only have an effect for + single-band animations, not three-band RGB animations). + colorbar_kwargs : dict, optional + An optional dict of keyword arguments used to control the + appearance of the colourbar. Keyword arguments are passed to + `matplotlib.pyplot.tick_params` + (see https://matplotlib.org/api/_as_gen/matplotlib.pyplot.tick_params.html + for options). This can be used to customise the colourbar + ticks, e.g. changing tick label colour depending on the + background of the animation: + `colorbar_kwargs={'colors': 'black'}`. + limit: int, optional + An optional integer specifying how many animation frames to + render (e.g. `limit=50` will render the first 50 frames). This + can be useful for quickly testing animations without rendering + the entire time-series. + """ + + def _start_end_times(gdf, ds): + """ + Converts 'start_time' and 'end_time' columns in a + `geopandas.GeoDataFrame` to datetime objects to allow vector + features to be plotted at specific moments in time during an + animation, and sets default values based on the first + and last time in `ds` if this information is missing from the + dataset. + """ + + # Make copy of gdf so we do not modify original data + gdf = gdf.copy() + + # Get min and max times from input dataset + minmax_times = pd.to_datetime(ds.time.isel(time=[0, -1]).values) + + # Update both `start_time` and `end_time` columns + for time_col, time_val in zip(["start_time", "end_time"], minmax_times): + + # Add time_col if it does not exist + if time_col not in gdf: + gdf[time_col] = np.nan + + # Convert values to datetimes and fill gaps with relevant time value + gdf[time_col] = pd.to_datetime(gdf[time_col], errors="ignore") + gdf[time_col] = gdf[time_col].fillna(time_val) + + return gdf + + def _add_colorbar(fig, ax, vmin, vmax, imshow_defaults, colorbar_defaults): + """ + Adds a new colorbar axis to the animation with custom minimum + and maximum values and styling. + """ + + # Create new axis object for colorbar + cax = fig.add_axes([0.02, 0.02, 0.96, 0.03]) + + # Initialise color bar using plot min and max values + img = ax.imshow(np.array([[vmin, vmax]]), **imshow_defaults) + fig.colorbar( + img, cax=cax, orientation="horizontal", ticks=np.linspace(vmin, vmax, 2) + ) + + # Fine-tune appearance of colorbar + cax.xaxis.set_ticks_position("top") + cax.tick_params(axis="x", **colorbar_defaults) + cax.get_xticklabels()[0].set_horizontalalignment("left") + cax.get_xticklabels()[-1].set_horizontalalignment("right") + + def _frame_annotation(times, show_date, show_text): + """ + Creates a custom annotation for the top-right of the animation + by converting a `xarray.DataArray` of times into strings, and + combining this with a custom text annotation. Handles cases + where `show_date=False/None`, `show_text=False/None`, or where + `show_text` is a list of strings. + """ + + # Test if show_text is supplied as a list + is_sequence = isinstance(show_text, (list, tuple, np.ndarray)) + + # Raise exception if it is shorter than number of dates + if is_sequence and (len(show_text) == 1): + show_text, is_sequence = show_text[0], False + elif is_sequence and (len(show_text) < len(times)): + raise ValueError( + f"Annotations supplied via `show_text` must have " + f"either a length of 1, or a length >= the number " + f"of timesteps in `ds` (n={len(times)})" + ) + + times_list = ( + times.dt.strftime(show_date).values if show_date else [None] * len(times) + ) + text_list = show_text if is_sequence else [show_text] * len(times) + annotation_list = [ + "\n".join([str(i) for i in (a, b) if i]) + for a, b in zip(times_list, text_list) + ] + + return annotation_list + + def _update_frames( + i, + ax, + extent, + annotation_text, + gdf, + gdf_defaults, + annotation_defaults, + imshow_defaults, + ): + """ + Animation called by `matplotlib.animation.FuncAnimation` to + animate each frame in the animation. Plots array and any text + annotations, as well as a temporal subset of `gdf` data based + on the times specified in 'start_time' and 'end_time' columns. + """ + + # Clear previous frame to optimise render speed and plot imagery + ax.clear() + ax.imshow( + array[i, ...].clip(0.0, 1.0), + extent=extent, + vmin=0.0, + vmax=1.0, + **imshow_defaults, + ) + + # Add annotation text + ax.annotate(annotation_text[i], **annotation_defaults) + + # Add geodataframe annotation + if show_gdf is not None: + + # Obtain start and end times to filter geodataframe features + time_i = ds.time.isel(time=i).values + + # Subset geodataframe using start and end dates + gdf_subset = show_gdf.loc[ + (show_gdf.start_time <= time_i) & (show_gdf.end_time >= time_i) + ] + + if len(gdf_subset.index) > 0: + + # Set color to geodataframe field if supplied + if ("color" in gdf_subset) and ("color" not in gdf_kwargs): + gdf_defaults.update({"color": gdf_subset["color"].tolist()}) + + gdf_subset.plot(ax=ax, **gdf_defaults) + + # Remove axes to show imagery only + ax.axis("off") + + # Update progress bar + progress_bar.update(1) + + # Test if bands have been supplied, or convert to list to allow + # iteration if a single band is provided as a string + if bands is None: + raise ValueError( + f"Please use the `bands` parameter to supply " + f"a list of one or three bands that exist as " + f"variables in `ds`, e.g. {list(ds.data_vars)}" + ) + elif isinstance(bands, str): + bands = [bands] + + # Test if bands exist in dataset + missing_bands = [b for b in bands if b not in ds.data_vars] + if missing_bands: + raise ValueError( + f"Band(s) {missing_bands} do not exist as " + f"variables in `ds` {list(ds.data_vars)}" + ) + + # Test if time dimension exists in dataset + if "time" not in ds.dims: + raise ValueError( + f"`ds` does not contain a 'time' dimension " + f"required for generating an animation" + ) + + # Set default parameters + outline = [PathEffects.withStroke(linewidth=2.5, foreground="black")] + annotation_defaults = { + "xy": (1, 1), + "xycoords": "axes fraction", + "xytext": (-5, -5), + "textcoords": "offset points", + "horizontalalignment": "right", + "verticalalignment": "top", + "fontsize": 20, + "color": "white", + "path_effects": outline, + } + imshow_defaults = {"cmap": "magma", "interpolation": "nearest"} + colorbar_defaults = {"colors": "white", "labelsize": 12, "length": 0} + gdf_defaults = {"linewidth": 1.5} + + # Update defaults with kwargs + annotation_defaults.update(annotation_kwargs) + imshow_defaults.update(imshow_kwargs) + colorbar_defaults.update(colorbar_kwargs) + gdf_defaults.update(gdf_kwargs) + + # Get info on dataset dimensions + height, width = ds.geobox.shape + scale = width_pixels / width + left, bottom, right, top = ds.geobox.extent.boundingbox + + # Prepare annotations + annotation_list = _frame_annotation(ds.time, show_date, show_text) + + # Prepare geodataframe + if show_gdf is not None: + show_gdf = show_gdf.to_crs(ds.geobox.crs) + show_gdf = gpd.clip(show_gdf, mask=box(left, bottom, right, top)) + show_gdf = _start_end_times(show_gdf, ds) + + # Convert data to 4D numpy array of shape [time, y, x, bands] + ds = ds[bands].to_array().transpose(..., "variable")[0:limit, ...] + array = ds.astype(np.float32).values + + # Optionally apply image processing along axis 0 (e.g. to each timestep) + bar_format = ( + "{l_bar}{bar}| {n_fmt}/{total_fmt} ({remaining_s:.1f} " + "seconds remaining at {rate_fmt}{postfix})" + ) + if image_proc_funcs: + print("Applying custom image processing functions") + for i, array_i in tqdm( + enumerate(array), + total=len(ds.time), + leave=False, + bar_format=bar_format, + unit=" frames", + ): + for func in image_proc_funcs: + array_i = func(array_i) + array[i, ...] = array_i + + # Clip to percentiles and rescale between 0.0 and 1.0 for plotting + vmin, vmax = np.quantile(array[np.isfinite(array)], q=percentile_stretch) + + # Replace with vmin and vmax if present in `imshow_defaults` + if "vmin" in imshow_defaults: + vmin = imshow_defaults.pop("vmin") + if "vmax" in imshow_defaults: + vmax = imshow_defaults.pop("vmax") + + # Rescale between 0 and 1 + array = rescale_intensity(array, in_range=(vmin, vmax), out_range=(0.0, 1.0)) + array = np.squeeze(array) # remove final axis if only one band + + # Set up figure + fig, ax = plt.subplots() + fig.set_size_inches(width * scale / 72, height * scale / 72, forward=True) + fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0) + + # Optionally add colorbar + if show_colorbar & (len(bands) == 1): + _add_colorbar(fig, ax, vmin, vmax, imshow_defaults, colorbar_defaults) + + # Animate + print(f"Exporting animation to {output_path}") + anim = FuncAnimation( + fig=fig, + func=_update_frames, + fargs=( + ax, # axis to plot into + [left, right, bottom, top], # imshow extent + annotation_list, # list of text annotations + show_gdf, # geodataframe to plot over imagery + gdf_defaults, # any kwargs used to plot gdf + annotation_defaults, # kwargs for annotations + imshow_defaults, + ), # kwargs for imshow + frames=len(ds.time), + interval=interval, + repeat=False, + ) + + # Set up progress bar + progress_bar = tqdm(total=len(ds.time), unit=" frames", bar_format=bar_format) + + # Export animation to file + if Path(output_path).suffix == ".gif": + anim.save(output_path, writer="pillow") + else: + anim.save(output_path, dpi=72) + + # Update progress bar to fix progress bar moving past end + if progress_bar.n != len(ds.time): + progress_bar.n = len(ds.time) + progress_bar.last_print_n = len(ds.time) + + +def _degree_to_zoom_level(l1, l2, margin=0.0): + """ + Helper function to set zoom level for `display_map` + """ + + degree = abs(l1 - l2) * (1 + margin) + zoom_level_int = 0 + if degree != 0: + zoom_level_float = math.log(360 / degree) / math.log(2) + zoom_level_int = int(zoom_level_float) + else: + zoom_level_int = 18 + return zoom_level_int + + +def plot_wofs(wofs, legend=True, **plot_kwargs): + """Plot a water observation bit flag image. + + Parameters + ---------- + wofs : xr.DataArray + A DataArray containing water observation bit flags. + legend : bool + Whether to plot a legend. Default True. + plot_kwargs : dict + Keyword arguments passed on to DataArray.plot. + + Returns + ------- + plot + """ + cmap = mcolours.ListedColormap( + [ + np.array([150, 150, 110]) / 255, # dry - 0 + np.array([0, 0, 0]) / 255, # nodata, - 1 + np.array([119, 104, 87]) / 255, # terrain - 16 + np.array([89, 88, 86]) / 255, # cloud_shadow - 32 + np.array([216, 215, 214]) / 255, # cloud - 64 + np.array([242, 220, 180]) / 255, # cloudy terrain - 80 + np.array([79, 129, 189]) / 255, # water - 128 + np.array([51, 82, 119]) / 255, # shady water - 160 + np.array([186, 211, 242]) / 255, # cloudy water - 192 + ] + ) + bounds = [ + 0, + 1, + 16, + 32, + 64, + 80, + 128, + 160, + 192, + ] + norm = mcolours.BoundaryNorm(np.array(bounds) - 0.1, cmap.N) + cblabels = [ + "dry", + "nodata", + "terrain", + "cloud shadow", + "cloud", + "cloudy terrain", + "water", + "shady water", + "cloudy water", + ] + + try: + im = wofs.plot.imshow(cmap=cmap, norm=norm, add_colorbar=legend, **plot_kwargs) + except AttributeError: + im = wofs.plot(cmap=cmap, norm=norm, add_colorbar=legend, **plot_kwargs) + + if legend: + try: + cb = im.colorbar + except AttributeError: + cb = im.cbar + ticks = cb.get_ticks() + cb.set_ticks(ticks + np.diff(ticks, append=193) / 2) + cb.set_ticklabels(cblabels) + return im + + +def plot_lulc(lulc, product=None, legend=True, **plot_kwargs): + """Plot a LULC image. + + Parameters + ---------- + lulc : xr.DataArray + A DataArray containing LULC bit flags. + product : str + 'ESA', 'IO', 'CGLS', or 'CCI', 'ESRI' + legend : bool + Whether to plot a legend. Default True. + plot_kwargs : dict + Keyword arguments passed on to DataArray.plot. + + Returns + ------- + plot + """ + + if "ESRI" in product: + # this is for the orignal ESRI/IO 10 class product for 2020 + try: + cmap = mcolours.ListedColormap( + [ + np.array([0, 0, 0]) / 255, + np.array([65, 155, 223]) / 255, + np.array([57, 125, 73]) / 255, + np.array([136, 176, 83]) / 255, + np.array([122, 135, 198]) / 255, + np.array([228, 150, 53]) / 255, + np.array([223, 195, 90]) / 255, + np.array([196, 40, 27]) / 255, + np.array([165, 155, 143]) / 255, + np.array([168, 235, 255]) / 255, + np.array([97, 97, 97]) / 255, + ] + ) + bounds = range(0, 12) + norm = mcolours.BoundaryNorm(np.array(bounds), cmap.N) + cblabels = [ + "no data", + "water", + "trees", + "grass", + "flooded vegetation", + "crops", + "scrub/shrub", + "built area", + "bare ground", + "snow/ice", + "clouds", + ] + except: + AttributeError + + if "IO" in product: + # this is for the ESRI/IO 9 class multiyear product; same color as preview + try: + cmap = mcolours.ListedColormap( + [ + np.array([0, 0, 0]) / 255, + np.array([65, 155, 223]) / 255, + np.array([57, 125, 73]) / 255, + np.array([122, 135, 198]) / 255, + np.array([228, 150, 53]) / 255, + np.array([196, 40, 27]) / 255, + np.array([165, 155, 143]) / 255, + np.array([168, 235, 255]) / 255, + np.array([97, 97, 97]) / 255, + np.array([227, 226, 195]) / 255, + ] + ) + bounds = [-0.5, 0.5, 1.5, 3, 4.5, 6, 7.5, 8.5, 9.5, 10.5, 11.5] + norm = mcolours.BoundaryNorm(np.array(bounds), cmap.N) + cblabels = [ + "no data", + "water", + "trees", + "flooded vegetation", + "crops", + "built area", + "bare ground", + "snow/ice", + "clouds", + "rangeland", + ] + ticks = list(np.mean((bounds[i+1], val)) for i, val in enumerate(bounds[:-1])) + except: + AttributeError + + if "ESA" in product: + try: + cmap = mcolours.ListedColormap( + [ + np.array([0, 0, 0]) / 255, + np.array([0, 100, 0]) / 255, + np.array([255, 187, 34]) / 255, + np.array([255, 255, 76]) / 255, + np.array([240, 150, 255]) / 255, + np.array([250, 0, 0]) / 255, + np.array([180, 180, 180]) / 255, + np.array([240, 240, 240]) / 255, + np.array([0, 100, 200]) / 255, + np.array([0, 150, 160]) / 255, + np.array([0, 207, 117]) / 255, + np.array([250, 230, 160]) / 255, + ] + ) + bounds = [-5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 92, 98, 105] + norm = mcolours.BoundaryNorm(np.array(bounds), cmap.N) + cblabels = [ + "no data", + "tree cover", + "shrubland", + "grassland", + "cropland", + "built up", + "bare/sparse vegetation", + "snow and ice", + "permanent water bodies", + "herbaceous wetland", + "mangroves", + "moss and lichen", + ] + except: + AttributeError + + if "CGLS" in product: + try: + labels = {0: {'color': '#282828', 'flag': 'unknown'}, + 20: {'color': '#FFBB22', 'flag': 'shrubs'}, + 30: {'color': '#FFFF4C', 'flag': 'herbaceous_vegetation'}, + 40: {'color': '#F096FF', 'flag': 'cultivated_and_managed_vegetation_or_agriculture'}, + 50: {'color': '#FA0000', 'flag': 'urban_or_built_up'}, + 60: {'color': '#B4B4B4', 'flag': 'bare_or_sparse_vegetation'}, + 70: {'color': '#F0F0F0', 'flag': 'snow_and_ice'}, + 80: {'color': '#0032C8', 'flag': 'permanent_water_bodies'}, + 90: {'color': '#0096A0', 'flag': 'herbaceous_wetland'}, + 100: {'color': '#FAE6A0', 'flag': 'moss_and_lichen'}, + 111: {'color': '#58481F', 'flag': 'closed_forest_evergreen_needle_leaf'}, + 112: {'color': '#009900', 'flag': 'closed_forest_evergreen_broad_leaf'}, + 113: {'color': '#70663E', 'flag': 'closed_forest_deciduous_needle_leaf'}, + 114: {'color': '#00CC00', 'flag': 'closed_forest_deciduous_broad_leaf'}, + 115: {'color': '#4E751F', 'flag': 'closed_forest_mixed'}, + 116: {'color': '#007800', 'flag': 'closed_forest_not_matching_any_of_the_other_definitions'}, + 121: {'color': '#666000', 'flag': 'open_forest_evergreen_needle_leaf'}, + 122: {'color': '#8DB400', 'flag': 'open_forest_evergreen_broad_leaf'}, + 123: {'color': '#8D7400', 'flag': 'open_forest_deciduous_needle_leaf'}, + 124: {'color': '#A0DC00', 'flag': 'open_forest_deciduous_broad_leaf'}, + 125: {'color': '#929900', 'flag': 'open_forest_mixed'}, + 126: {'color': '#648C00', 'flag': 'open_forest_not_matching_any_of_the_other_definitions'}, + 200: {'color': '#000080', 'flag': 'oceans_seas'}} + + colors = [label['color'] for label in labels.values()] + cmap = ListedColormap([label['color'] for label in labels.values()]) + norm = mcolours.BoundaryNorm(list(labels.keys())+[201], cmap.N+1, extend='max') + ticks = list(np.mean((list(list(labels.keys())+[201])[i+1], val)) for i, val in enumerate(list(labels.keys()))) + cblabels=[label['flag'] for label in labels.values()] + + except: + AttributeError + if 'CCI' in product: + try: + labels = {0: {'color': '#282828', 'flag': 'no data'}, + 10: {'color': '#EBEB34', 'flag': 'cropland, rainfed'}, + 11: {'color': '#D9EB34', 'flag': 'cropland, rainfed, herbaceous cover'}, + 12: {'color': '#EBDF34', 'flag': 'cropland, rainfed, tree or shrub cover'}, + 20: {'color': '#34EBE2', 'flag': 'cropland, irrigated or post-flooding'}, + 30: {'color': '#EBBD34', 'flag': 'mosaic cropland/natural vegetation'}, + 40: {'color': '#eba534', 'flag': 'mosaic natural vegetation/cropland'}, + 50: {'color': '#34eb46', 'flag': 'tree cover, broadleaved, evergreen, closed to open'}, + 60: {'color': '#21750e', 'flag': 'tree cover, broadleaved, deciduous, closed to open'}, + 61: {'color': '#449432', 'flag': 'tree cover, broadleaved, deciduous, closed'}, + 62: {'color': '#5da64c', 'flag': 'tree cover, broadleaved, deciduous, open'}, + 70: {'color': '#16470b', 'flag': 'tree cover, needleleaved, evergreen, closed to open'}, + 71: {'color': '#237012', 'flag': 'tree cover, needleleaved, evergreen, closed'}, + 72: {'color': '#237012', 'flag': 'tree cover, needleleaved, evergreen, open'}, + 80: {'color': '#31a317', 'flag': 'tree cover, needleleaved, deciduous, closed to open'}, + 81: {'color': '#57ed34', 'flag': 'tree cover, needleleaved, deciduous, closed'}, + 82: {'color': '#81f765', 'flag': 'tree cover, needleleaved, deciduous, open'}, + 90: {'color': '#b6ed64', 'flag': 'tree cover, mixed leaf type'}, + 100: {'color': '#6f8f3f', 'flag': 'mosaic tree and shrub/herbaceous cover'}, + 110: {'color': '#ad950c', 'flag': 'mosaic herbaceous cover/tree and shrub'}, + 120: {'color': '#5e5209', 'flag': 'shrubland'}, + 121: {'color': '#292302', 'flag': 'shrubland, evergreen'}, + 122: {'color': '#a89008', 'flag': 'shrubland, deciduous'}, + 130: {'color': '#f7bf07', 'flag': 'grassland'}, + 140: {'color': '#f57feb', 'flag': 'lichens and mosses'}, + 150: {'color': '#f57feb', 'flag': 'sparse vegetation'}, + 151: {'color': '#fcf7a4', 'flag': 'sparse tree'}, + 152: {'color': '#d4cf87', 'flag': 'sparse shrub'}, + 153: {'color': '#b0aa54', 'flag': 'sparse herbaceous cover'}, + 160: {'color': '#159638', 'flag': 'tree cover, flooded, fresh or brakish water'}, + 170: {'color': '#22bf81', 'flag': 'tree cover, flooded, saline water'}, + 180: {'color': '#44eba9', 'flag': 'shrub or herbaceous cover, flooded, fresh/saline/brakish water'}, + 190: {'color': '#a3273c', 'flag': 'urban areas'}, + 200: {'color': '#fffbcc', 'flag': 'bare areas'}, + 201: {'color': '#b0afa4', 'flag': 'consolidated bare areas'}, + 202: {'color': '#d6d4b6', 'flag': 'unconsolidated bare areas'}, + 210: {'color': '#1A3EF0', 'flag': 'water bodies'}, + 220: {'color': '#ffffff', 'flag': 'permanent snow and ice'}} + + colors = [label['color'] for label in labels.values()] + cmap = ListedColormap([label['color'] for label in labels.values()]) + norm = mcolours.BoundaryNorm(list(labels.keys())+[221], cmap.N+1, extend='max') + ticks = list(np.mean((list(list(labels.keys())+[221])[i+1], val)) for i, val in enumerate(list(labels.keys()))) + cblabels=[label['flag'] for label in labels.values()] + + except: + AttributeError + + try: + im = lulc.plot.imshow(cmap=cmap, norm=norm, add_colorbar=legend, **plot_kwargs) + except AttributeError: + im = lulc.plot(cmap=cmap, norm=norm, add_colorbar=legend, **plot_kwargs) + + if legend: + try: + cb = im.colorbar + except AttributeError: + cb = im.cbar + + if "ESRI" in product: + cb.set_ticks(np.arange(0, 11, 1)+0.5) + cb.set_ticklabels(cblabels) + + if "IO" in product: + cb.set_ticks(ticks) + cb.set_ticklabels(cblabels) + + if "ESA" in product: + cb.set_ticks([0, 10, 20, 30, 40, 50, 60, 70, 80, 88.5, 95, 101.5]) + cb.set_ticklabels(cblabels) + + if "CGLS" in product: + cb.set_ticks(ticks) + cb.set_ticklabels(cblabels) + + if "CCI" in product: + cb.set_ticks(ticks) + cb.set_ticklabels(cblabels) + + return im diff --git a/deafrica_tools/spatial.py b/deafrica_tools/spatial.py new file mode 100644 index 0000000..de454f2 --- /dev/null +++ b/deafrica_tools/spatial.py @@ -0,0 +1,949 @@ +''' +Spatial analyses functions for Digital Earth Africa data. +''' + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os + +os.environ['USE_PYGEOS'] = '0' + +import multiprocessing as mp + +import dask +import fiona +import geopandas as gpd +import numpy as np +import odc.geo.xr # adds `.odc.x` attributes to our xarray objects. +import pandas as pd +import rasterio.features +import scipy.interpolate +import xarray as xr +from datacube.api.query import query_group_by +from datacube.model.utils import xr_apply +from datacube.utils.cog import write_cog +from datacube.utils.geometry import CRS, Geometry +from geopy.geocoders import Nominatim +from rasterstats import zonal_stats +from shapely.geometry import LineString, MultiLineString, mapping, shape +from skimage.measure import find_contours, label + + +def add_geobox(ds, crs=None): + """ + Ensure that an xarray DataArray has a GeoBox and .odc.* accessor + using `odc.geo`. + + If `ds` is missing a Coordinate Reference System (CRS), this can be + supplied using the `crs` param. + + Parameters + ---------- + ds : xarray.Dataset or xarray.DataArray + Input xarray object that needs to be checked for spatial + information. + crs : str, optional + Coordinate Reference System (CRS) information for the input `ds` + array. If `ds` already has a CRS, then `crs` is not required. + Default is None. + + Returns + ------- + xarray.Dataset or xarray.DataArray + The input xarray object with added `.odc.x` attributes to access + spatial information. + + """ + # If a CRS is not found, use custom provided CRS + if ds.odc.crs is None and crs is not None: + ds = ds.odc.assign_crs(crs) + elif ds.odc.crs is None and crs is None: + raise ValueError( + "Unable to determine `ds`'s coordinate " + "reference system (CRS). Please provide a " + "CRS using the `crs` parameter " + "(e.g. `crs='EPSG:3577'`)." + ) + + return ds + + +def xr_vectorize( + da, + attribute_col=None, + crs=None, + dtype="float32", + output_path=None, + verbose=True, + **rasterio_kwargs, +): + """ + Vectorises a raster ``xarray.DataArray`` into a vector + ``geopandas.GeoDataFrame``. + + Parameters + ---------- + da : xarray.DataArray + The input ``xarray.DataArray`` data to vectorise. + attribute_col : str, optional + Name of the attribute column in the resulting + ``geopandas.GeoDataFrame``. Values from ``da`` converted + to polygons will be assigned to this column. If None, + the column name will default to 'attribute'. + crs : str or CRS object, optional + If ``da``'s coordinate reference system (CRS) cannot be + determined, provide a CRS using this parameter. + (e.g. 'EPSG:3577'). + dtype : str, optional + Data type of must be one of int16, int32, uint8, uint16, + or float32 + output_path : string, optional + Provide an optional string file path to export the vectorised + data to file. Supports any vector file formats supported by + ``geopandas.GeoDataFrame.to_file()``. + verbose : bool, optional + Print debugging messages. Default True. + **rasterio_kwargs : + A set of keyword arguments to ``rasterio.features.shapes``. + Can include `mask` and `connectivity`. + + Returns + ------- + gdf : geopandas.GeoDataFrame + + """ + + # Add GeoBox and odc.* accessor to array using `odc-geo` + da = add_geobox(da, crs) + + # Run the vectorizing function + vectors = rasterio.features.shapes( + source=da.data.astype(dtype), transform=da.odc.transform, **rasterio_kwargs + ) + + # Convert the generator into a list + vectors = list(vectors) + + # Extract the polygon coordinates and values from the list + polygons = [polygon for polygon, value in vectors] + values = [value for polygon, value in vectors] + + # Convert polygon coordinates into polygon shapes + polygons = [shape(polygon) for polygon in polygons] + + # Create a geopandas dataframe populated with the polygon shapes + attribute_name = attribute_col if attribute_col is not None else "attribute" + gdf = gpd.GeoDataFrame( + data={attribute_name: values}, geometry=polygons, crs=da.odc.crs + ) + + # If a file path is supplied, export to file + if output_path is not None: + if verbose: + print(f"Exporting vector data to {output_path}") + gdf.to_file(output_path) + + return gdf + + +def xr_rasterize( + gdf, + da, + attribute_col=None, + crs=None, + name=None, + output_path=None, + verbose=True, + **rasterio_kwargs, +): + """ + Rasterizes a vector ``geopandas.GeoDataFrame`` into a + raster ``xarray.DataArray``. + + Parameters + ---------- + gdf : geopandas.GeoDataFrame + A ``geopandas.GeoDataFrame`` object containing the vector + data you want to rasterise. + da : xarray.DataArray or xarray.Dataset + The shape, coordinates, dimensions, and transform of this object + are used to define the array that ``gdf`` is rasterized into. + It effectively provides a spatial template. + attribute_col : string, optional + Name of the attribute column in ``gdf`` containing values for + each vector feature that will be rasterized. If None, the + output will be a boolean array of 1's and 0's. + crs : str or CRS object, optional + If ``da``'s coordinate reference system (CRS) cannot be + determined, provide a CRS using this parameter. + (e.g. 'EPSG:3577'). + name : str, optional + An optional name used for the output ``xarray.DataArray`. + output_path : string, optional + Provide an optional string file path to export the rasterized + data as a GeoTIFF file. + verbose : bool, optional + Print debugging messages. Default True. + **rasterio_kwargs : + A set of keyword arguments to ``rasterio.features.rasterize``. + Can include: 'all_touched', 'merge_alg', 'dtype'. + + Returns + ------- + da_rasterized : xarray.DataArray + The rasterized vector data. + """ + + # Add GeoBox and odc.* accessor to array using `odc-geo` + da = add_geobox(da, crs) + + # Reproject vector data to raster's CRS + gdf_reproj = gdf.to_crs(crs=da.odc.crs) + + # If an attribute column is specified, rasterise using vector + # attribute values. Otherwise, rasterise into a boolean array + if attribute_col is not None: + # Use the geometry and attributes from `gdf` to create an iterable + shapes = zip(gdf_reproj.geometry, gdf_reproj[attribute_col]) + else: + # Use geometry directly (will produce a boolean numpy array) + shapes = gdf_reproj.geometry + + # Rasterise shapes into a numpy array + im = rasterio.features.rasterize( + shapes=shapes, + out_shape=da.odc.geobox.shape, + transform=da.odc.geobox.transform, + **rasterio_kwargs, + ) + + # Convert numpy array to a full xarray.DataArray + # and set array name if supplied + da_rasterized = odc.geo.xr.wrap_xr(im=im, gbox=da.odc.geobox) + da_rasterized = da_rasterized.rename(name) + + # If a file path is supplied, export to file + if output_path is not None: + if verbose: + print(f"Exporting raster data to {output_path}") + write_cog(da_rasterized, output_path, overwrite=True) + + return da_rasterized + + +def subpixel_contours( + da, + z_values=[0.0], + crs=None, + attribute_df=None, + output_path=None, + min_vertices=2, + dim="time", + time_format="%Y-%m-%d", + errors="ignore", + verbose=True, +): + """ + Uses `skimage.measure.find_contours` to extract multiple z-value + contour lines from a two-dimensional array (e.g. multiple elevations + from a single DEM), or one z-value for each array along a specified + dimension of a multi-dimensional array (e.g. to map waterlines + across time by extracting a 0 NDWI contour from each individual + timestep in an xarray timeseries). + + Contours are returned as a geopandas.GeoDataFrame with one row per + z-value or one row per array along a specified dimension. The + `attribute_df` parameter can be used to pass custom attributes + to the output contour features. + + Last modified: May 2023 + + Parameters + ---------- + da : xarray DataArray + A two-dimensional or multi-dimensional array from which + contours are extracted. If a two-dimensional array is provided, + the analysis will run in 'single array, multiple z-values' mode + which allows you to specify multiple `z_values` to be extracted. + If a multi-dimensional array is provided, the analysis will run + in 'single z-value, multiple arrays' mode allowing you to + extract contours for each array along the dimension specified + by the `dim` parameter. + z_values : int, float or list of ints, floats + An individual z-value or list of multiple z-values to extract + from the array. If operating in 'single z-value, multiple + arrays' mode specify only a single z-value. + crs : string or CRS object, optional + If ``da``'s coordinate reference system (CRS) cannot be + determined, provide a CRS using this parameter. + (e.g. 'EPSG:3577'). + output_path : string, optional + The path and filename for the output shapefile. + attribute_df : pandas.Dataframe, optional + A pandas.Dataframe containing attributes to pass to the output + contour features. The dataframe must contain either the same + number of rows as supplied `z_values` (in 'multiple z-value, + single array' mode), or the same number of rows as the number + of arrays along the `dim` dimension ('single z-value, multiple + arrays mode'). + min_vertices : int, optional + The minimum number of vertices required for a contour to be + extracted. The default (and minimum) value is 2, which is the + smallest number required to produce a contour line (i.e. a start + and end point). Higher values remove smaller contours, + potentially removing noise from the output dataset. + dim : string, optional + The name of the dimension along which to extract contours when + operating in 'single z-value, multiple arrays' mode. The default + is 'time', which extracts contours for each array along the time + dimension. + time_format : string, optional + The format used to convert `numpy.datetime64` values to strings + if applied to data with a "time" dimension. Defaults to + "%Y-%m-%d". + errors : string, optional + If 'raise', then any failed contours will raise an exception. + If 'ignore' (the default), a list of failed contours will be + printed. If no contours are returned, an exception will always + be raised. + verbose : bool, optional + Print debugging messages. Default is True. + + Returns + ------- + output_gdf : geopandas geodataframe + A geopandas geodataframe object with one feature per z-value + ('single array, multiple z-values' mode), or one row per array + along the dimension specified by the `dim` parameter ('single + z-value, multiple arrays' mode). If `attribute_df` was + provided, these values will be included in the shapefile's + attribute table. + """ + + def _contours_to_multiline(da_i, z_value, min_vertices=2): + """ + Helper function to apply marching squares contour extraction + to an array and return a data as a shapely MultiLineString. + The `min_vertices` parameter allows you to drop small contours + with less than X vertices. + """ + + # Extracts contours from array, and converts each discrete + # contour into a Shapely LineString feature. If the function + # returns a KeyError, this may be due to an unresolved issue in + # scikit-image: https://github.com/scikit-image/scikit-image/issues/4830 + # A temporary workaround is to peturb the z-value by a tiny + # amount (1e-12) before using it to extract the contour. + try: + line_features = [ + LineString(i[:, [1, 0]]) + for i in find_contours(da_i.data, z_value) + if i.shape[0] >= min_vertices + ] + except KeyError: + line_features = [ + LineString(i[:, [1, 0]]) + for i in find_contours(da_i.data, z_value + 1e-12) + if i.shape[0] >= min_vertices + ] + + # Output resulting lines into a single combined MultiLineString + return MultiLineString(line_features) + + def _time_format(i, time_format): + """ + Converts numpy.datetime64 into formatted strings; + otherwise returns data as-is. + """ + if isinstance(i, np.datetime64): + ts = pd.to_datetime(str(i)) + i = ts.strftime(time_format) + return i + + # Verify input data is a xr.DataArray + if not isinstance(da, xr.DataArray): + raise ValueError( + "The input `da` is not an xarray.DataArray. " + "If you supplied an xarray.Dataset, pass in one " + "of its data variables using the syntax " + "`da=ds.`." + ) + + # Add GeoBox and odc.* accessor to array using `odc-geo` + da = add_geobox(da, crs) + + # If z_values is supplied is not a list, convert to list: + z_values = ( + z_values + if (isinstance(z_values, list) or isinstance(z_values, np.ndarray)) + else [z_values] + ) + + # If dask collection, load into memory + if dask.is_dask_collection(da): + if verbose: + print("Loading data into memory using Dask") + da = da.compute() + + # Test number of dimensions in supplied data array + if len(da.shape) == 2: + if verbose: + print("Operating in multiple z-value, single array mode") + dim = "z_value" + contour_arrays = { + _time_format(i, time_format): _contours_to_multiline(da, i, min_vertices) + for i in z_values + } + + else: + # Test if only a single z-value is given when operating in + # single z-value, multiple arrays mode + if verbose: + print("Operating in single z-value, multiple arrays mode") + if len(z_values) > 1: + raise ValueError( + "Please provide a single z-value when operating " + "in single z-value, multiple arrays mode" + ) + + contour_arrays = { + _time_format(i, time_format): _contours_to_multiline( + da_i, z_values[0], min_vertices + ) + for i, da_i in da.groupby(dim) + } + + # If attributes are provided, add the contour keys to that dataframe + if attribute_df is not None: + try: + attribute_df.insert(0, dim, contour_arrays.keys()) + + # If this fails, it is due to the applied attribute table not + # matching the structure of the loaded data + except ValueError: + if len(da.shape) == 2: + raise ValueError( + f"The provided `attribute_df` contains a different " + f"number of rows ({len(attribute_df.index)}) " + f"than the number of supplied `z_values` " + f"({len(z_values)})." + ) + else: + raise ValueError( + f"The provided `attribute_df` contains a different " + f"number of rows ({len(attribute_df.index)}) " + f"than the number of arrays along the '{dim}' " + f"dimension ({len(da[dim])})." + ) + + # Otherwise, use the contour keys as the only main attributes + else: + attribute_df = list(contour_arrays.keys()) + + # Convert output contours to a geopandas.GeoDataFrame + contours_gdf = gpd.GeoDataFrame( + data=attribute_df, geometry=list(contour_arrays.values()), crs=da.odc.crs + ) + + # Define affine and use to convert array coords to geographic coords. + # We need to add 0.5 x pixel size to the x and y to obtain the centre + # point of our pixels, rather than the top-left corner + affine = da.odc.geobox.transform + shapely_affine = [ + affine.a, + affine.b, + affine.d, + affine.e, + affine.xoff + affine.a / 2.0, + affine.yoff + affine.e / 2.0, + ] + contours_gdf["geometry"] = contours_gdf.affine_transform(shapely_affine) + + # Rename the data column to match the dimension + contours_gdf = contours_gdf.rename({0: dim}, axis=1) + + # Drop empty timesteps + empty_contours = contours_gdf.geometry.is_empty + failed = ", ".join(map(str, contours_gdf[empty_contours][dim].to_list())) + contours_gdf = contours_gdf[~empty_contours] + + # Raise exception if no data is returned, or if any contours fail + # when `errors='raise'. Otherwise, print failed contours + if empty_contours.all() and errors == "raise": + raise ValueError( + "Failed to generate any valid contours; verify that " + "values passed to `z_values` are valid and present " + "in `da`" + ) + elif empty_contours.all() and errors == "ignore": + if verbose: + print( + "Failed to generate any valid contours; verify that " + "values passed to `z_values` are valid and present " + "in `da`" + ) + elif empty_contours.any() and errors == "raise": + raise Exception(f"Failed to generate contours: {failed}") + elif empty_contours.any() and errors == "ignore": + if verbose: + print(f"Failed to generate contours: {failed}") + + # If asked to write out file, test if GeoJSON or ESRI Shapefile. If + # GeoJSON, convert to EPSG:4326 before exporting. + if output_path and output_path.endswith(".geojson"): + if verbose: + print(f"Writing contours to {output_path}") + contours_gdf.to_crs("EPSG:4326").to_file(filename=output_path) + + if output_path and output_path.endswith(".shp"): + if verbose: + print(f"Writing contours to {output_path}") + contours_gdf.to_file(filename=output_path) + + return contours_gdf + + +def interpolate_2d(ds, + x_coords, + y_coords, + z_coords, + method='linear', + factor=1, + verbose=False, + **kwargs): + + """ + This function takes points with X, Y and Z coordinates, and + interpolates Z-values across the extent of an existing xarray + dataset. This can be useful for producing smooth surfaces from point + data that can be compared directly against satellite data derived + from an OpenDataCube query. + + Supported interpolation methods include 'linear', 'nearest' and + 'cubic (using `scipy.interpolate.griddata`), and 'rbf' (using + `scipy.interpolate.Rbf`). + + Last modified: February 2020 + + Parameters + ---------- + ds : xarray DataArray or Dataset + A two-dimensional or multi-dimensional array from which x and y + dimensions will be copied and used for the area in which to + interpolate point data. + x_coords, y_coords : numpy array + Arrays containing X and Y coordinates for all points (e.g. + longitudes and latitudes). + z_coords : numpy array + An array containing Z coordinates for all points (e.g. + elevations). These are the values you wish to interpolate + between. + method : string, optional + The method used to interpolate between point values. This string + is either passed to `scipy.interpolate.griddata` (for 'linear', + 'nearest' and 'cubic' methods), or used to specify Radial Basis + Function interpolation using `scipy.interpolate.Rbf` ('rbf'). + Defaults to 'linear'. + factor : int, optional + An optional integer that can be used to subsample the spatial + interpolation extent to obtain faster interpolation times, then + up-sample this array back to the original dimensions of the + data as a final step. For example, setting `factor=10` will + interpolate data into a grid that has one tenth of the + resolution of `ds`. This approach will be significantly faster + than interpolating at full resolution, but will potentially + produce less accurate or reliable results. + verbose : bool, optional + Print debugging messages. Default False. + **kwargs : + Optional keyword arguments to pass to either + `scipy.interpolate.griddata` (if `method` is 'linear', 'nearest' + or 'cubic'), or `scipy.interpolate.Rbf` (is `method` is 'rbf'). + + Returns + ------- + interp_2d_array : xarray DataArray + An xarray DataArray containing with x and y coordinates copied + from `ds_array`, and Z-values interpolated from the points data. + """ + + # Extract xy and elev points + points_xy = np.vstack([x_coords, y_coords]).T + + # Extract x and y coordinates to interpolate into. + # If `factor` is greater than 1, the coordinates will be subsampled + # for faster run-times. If the last x or y value in the subsampled + # grid aren't the same as the last x or y values in the original + # full resolution grid, add the final full resolution grid value to + # ensure data is interpolated up to the very edge of the array + if ds.x[::factor][-1].item() == ds.x[-1].item(): + x_grid_coords = ds.x[::factor].values + else: + x_grid_coords = ds.x[::factor].values.tolist() + [ds.x[-1].item()] + + if ds.y[::factor][-1].item() == ds.y[-1].item(): + y_grid_coords = ds.y[::factor].values + else: + y_grid_coords = ds.y[::factor].values.tolist() + [ds.y[-1].item()] + + # Create grid to interpolate into + grid_y, grid_x = np.meshgrid(x_grid_coords, y_grid_coords) + + # Apply scipy.interpolate.griddata interpolation methods + if method in ('linear', 'nearest', 'cubic'): + + # Interpolate x, y and z values + interp_2d = scipy.interpolate.griddata(points=points_xy, + values=z_coords, + xi=(grid_y, grid_x), + method=method, + **kwargs) + + # Apply Radial Basis Function interpolation + elif method == 'rbf': + + # Interpolate x, y and z values + rbf = scipy.interpolate.Rbf(x_coords, y_coords, z_coords, **kwargs) + interp_2d = rbf(grid_y, grid_x) + + # Create xarray dataarray from the data and resample to ds coords + interp_2d_da = xr.DataArray(interp_2d, + coords=[y_grid_coords, x_grid_coords], + dims=['y', 'x']) + + # If factor is greater than 1, resample the interpolated array to + # match the input `ds` array + if factor > 1: + interp_2d_da = interp_2d_da.interp_like(ds) + + return interp_2d_da + + +def contours_to_arrays(gdf, col): + """ + This function converts a polyline shapefile into an array with three + columns giving the X, Y and Z coordinates of each vertex. This data + can then be used as an input to interpolation procedures (e.g. using + a function like `interpolate_2d`. + + Last modified: October 2021 + + Parameters + ---------- + gdf : Geopandas GeoDataFrame + A GeoPandas GeoDataFrame of lines to convert into point + coordinates. + col : str + A string giving the name of the GeoDataFrame field to use as + Z-values. + + Returns + ------- + A numpy array with three columns giving the X, Y and Z coordinates + of each vertex in the input GeoDataFrame. + + """ + + # Explode multi-part geometries into multiple single geometries. + gdf = gdf.explode(ignore_index=True) + + coords_zvals = [] + + for i in range(0, len(gdf)): + val = gdf.iloc[i][col] + + try: + coords = np.concatenate( + [np.vstack(x.coords.xy).T for x in gdf.iloc[i].geometry.geoms] + ) + except Exception: + coords = np.vstack(gdf.iloc[i].geometry.coords.xy).T + + coords_zvals.append( + np.column_stack((coords, np.full(np.shape(coords)[0], fill_value=val))) + ) + + return np.concatenate(coords_zvals) + + +def largest_region(bool_array, **kwargs): + + ''' + Takes a boolean array and identifies the largest contiguous region of + connected True values. This is returned as a new array with cells in + the largest region marked as True, and all other cells marked as False. + + Parameters + ---------- + bool_array : boolean array + A boolean array (numpy or xarray.DataArray) with True values for + the areas that will be inspected to find the largest group of + connected cells + **kwargs : + Optional keyword arguments to pass to `measure.label` + + Returns + ------- + largest_region : boolean array + A boolean array with cells in the largest region marked as True, + and all other cells marked as False. + + ''' + + # First, break boolean array into unique, discrete regions/blobs + blobs_labels = label(bool_array, background=0, **kwargs) + + # Count the size of each blob, excluding the background class (0) + ids, counts = np.unique(blobs_labels[blobs_labels > 0], + return_counts=True) + + # Identify the region ID of the largest blob + largest_region_id = ids[np.argmax(counts)] + + # Produce a boolean array where 1 == the largest region + largest_region = blobs_labels == largest_region_id + + return largest_region + + +def transform_geojson_wgs_to_epsg(geojson, EPSG): + """ + Takes a geojson dictionary and converts it from WGS84 (EPSG:4326) to desired EPSG + + Parameters + ---------- + geojson: dict + a geojson dictionary containing a 'geometry' key, in WGS84 coordinates + EPSG: int + numeric code for the EPSG coordinate referecnce system to transform into + + Returns + ------- + transformed_geojson: dict + a geojson dictionary containing a 'coordinates' key, in the desired CRS + + """ + gg = Geometry(geojson['geometry'], CRS('epsg:4326')) + gg = gg.to_crs(CRS(f'epsg:{EPSG}')) + return gg.__geo_interface__ + + +def zonal_stats_parallel(shp, + raster, + statistics, + out_shp, + ncpus, + **kwargs): + + """ + Summarizing raster datasets based on vector geometries in parallel. + Each cpu recieves an equal chunk of the dataset. + Utilizes the perrygeo/rasterstats package. + + Parameters + ---------- + shp : str + Path to shapefile that contains polygons over + which zonal statistics are calculated + raster: str + Path to the raster from which the statistics are calculated. + This can be a virtual raster (.vrt). + statistics: list + list of statistics to calculate. e.g. + ['min', 'max', 'median', 'majority', 'sum'] + out_shp: str + Path to export shapefile containing zonal statistics. + ncpus: int + number of cores to parallelize the operations over. + kwargs: + Any other keyword arguments to rasterstats.zonal_stats() + See https://github.com/perrygeo/python-rasterstats for + all options + + Returns + ------- + Exports a shapefile to disk containing the zonal statistics requested + + """ + + # yields n sized chunks from list l (used for splitting task to multiple processes) + def chunks(l, n): + for i in range(0, len(l), n): + yield l[i:i + n] + + # calculates zonal stats and adds results to a dictionary + def worker(z, raster, d): + z_stats = zonal_stats(z, raster, stats=statistics, **kwargs) + for i in range(0, len(z_stats)): + d[z[i]['id']] = z_stats[i] + + # write output polygon + def write_output(zones, out_shp, d): + # copy schema and crs from input and add new fields for each statistic + schema = zones.schema.copy() + crs = zones.crs + for stat in statistics: + schema['properties'][stat] = 'float' + + with fiona.open(out_shp, 'w', 'ESRI Shapefile', schema, crs) as output: + for elem in zones: + for stat in statistics: + elem['properties'][stat] = d[elem['id']][stat] + output.write({'properties': elem['properties'], 'geometry': mapping(shape(elem['geometry']))}) + + with fiona.open(shp) as zones: + jobs = [] + + # create manager dictionary (polygon ids=keys, stats=entries) + # where multiple processes can write without conflicts + man = mp.Manager() + d = man.dict() + + # split zone polygons into 'ncpus' chunks for parallel processing + # and call worker() for each + split = chunks(zones, len(zones)//ncpus) + for z in split: + p = mp.Process(target=worker, args=(z, raster, d)) + p.start() + jobs.append(p) + + # wait that all chunks are finished + [j.join() for j in jobs] + + write_output(zones, out_shp, d) + + +def reverse_geocode(coords, site_classes=None, state_classes=None): + """ + Takes a latitude and longitude coordinate, and performs a reverse + geocode to return a plain-text description of the location in the + form: + + Site, State + + E.g.: `reverse_geocode(coords=(-35.282163, 149.128835))` + + 'Canberra, Australian Capital Territory' + + Parameters + ---------- + coords : tuple of floats + A tuple of (latitude, longitude) coordinates used to perform + the reverse geocode. + site_classes : list of strings, optional + A list of strings used to define the site part of the plain + text location description. Because the contents of the geocoded + address can vary greatly depending on location, these strings + are tested against the address one by one until a match is made. + + Defaults to: + + ``['city', 'town', 'village', 'suburb', 'hamlet', 'county', 'municipality']`` + + state_classes : list of strings, optional + A list of strings used to define the state part of the plain + text location description. These strings are tested against the + address one by one until a match is made. Defaults to: + `['state', 'territory']`. + Returns + ------- + If a valid geocoded address is found, a plain text location + description will be returned: + + 'Site, State' + + If no valid address is found, formatted coordinates will be returned + instead: + + 'XX.XX S, XX.XX E' + """ + + # Run reverse geocode using coordinates + geocoder = Nominatim(user_agent='Digital Earth Africa') + out = geocoder.reverse(coords) + + # Create plain text-coords as fall-back + lat = f'{-coords[0]:.2f} S' if coords[0] < 0 else f'{coords[0]:.2f} N' + lon = f'{-coords[1]:.2f} W' if coords[1] < 0 else f'{coords[1]:.2f} E' + + try: + + # Get address from geocoded data + address = out.raw['address'] + + # Use site and state classes if supplied; else use defaults + default_site_classes = ['city', 'town', 'village', 'suburb', 'hamlet', + 'county', 'municipality'] + default_state_classes = ['state', 'territory'] + site_classes = site_classes if site_classes else default_site_classes + state_classes = state_classes if state_classes else default_state_classes + + # Return the first site or state class that exists in address dict + site = next((address[k] for k in site_classes if k in address), None) + state = next((address[k] for k in state_classes if k in address), None) + + # If site and state exist in the data, return this. + # Otherwise, return N/E/S/W coordinates. + if site and state: + + # Return as site, state formatted string + return f'{site}, {state}' + + else: + + # If no geocoding result, return N/E/S/W coordinates + print('No valid geocoded location; returning coordinates instead') + return f'{lat}, {lon}' + + except (KeyError, AttributeError): + + # If no geocoding result, return N/E/S/W coordinates + print('No valid geocoded location; returning coordinates instead') + return f'{lat}, {lon}' + + +def sun_angles(dc, query): + """ + For a given spatiotemporal query, calculate mean sun + azimuth and elevation for each satellite observation, and + return these as a new `xarray.Dataset` with 'sun_elevation' + and 'sun_azimuth' variables. + + Parameters: + ----------- + dc : datacube.Datacube object + Datacube instance used to load data. + query : dict + A dictionary containing query parameters used to identify + satellite observations and load metadata. + + Returns: + -------- + sun_angles_ds : xarray.Dataset + An `xarray.set` containing a 'sun_elevation' and + 'sun_azimuth' variables. + """ + # Identify satellite datasets and group outputs using the + # same approach used to group satellite imagery (i.e. solar day) + gb = query_group_by(**query) + datasets = dc.find_datasets(**query) + dataset_array = dc.group_datasets(datasets, gb) + + # Load and take the mean of metadata from each product + sun_azimuth = xr_apply( + dataset_array, + lambda t, dd: np.mean([d.metadata.eo_sun_azimuth for d in dd]), + dtype=float, + ) + sun_elevation = xr_apply( + dataset_array, + lambda t, dd: np.mean([d.metadata.eo_sun_elevation for d in dd]), + dtype=float, + ) + + # Combine into new xarray.Dataset + sun_angles_ds = xr.merge( + [sun_elevation.rename("sun_elevation"), sun_azimuth.rename("sun_azimuth")] + ) + + return sun_angles_ds diff --git a/deafrica_tools/temporal.py b/deafrica_tools/temporal.py new file mode 100644 index 0000000..6539451 --- /dev/null +++ b/deafrica_tools/temporal.py @@ -0,0 +1,576 @@ +""" +Functions for calculating per-pixel temporal summary statistics on a +timeseries stored in a xarray.DataArray. + +The key functions are: + +.. autosummary:: + :caption: Primary functions + :nosignatures: + :toctree: gen + + xr_phenology + temporal_statistics + +.. autosummary:: + :nosignatures: + :toctree: gen + +""" + +import sys +import dask +import numpy as np +import xarray as xr +import hdstats +from packaging import version +from datacube.utils.geometry import assign_crs + + +def allNaN_arg(da, dim, stat): + """ + Calculate da.argmax() or da.argmin() while handling + all-NaN slices. Fills all-NaN locations with an + float and then masks the offending cells. + + Parameters + ---------- + da : xarray.DataArray + dim : str + Dimension over which to calculate argmax, argmin e.g. 'time' + stat : str + The statistic to calculte, either 'min' for argmin() + or 'max' for .argmax() + + Returns + ------- + xarray.DataArray + """ + # generate a mask where entire axis along dimension is NaN + mask = da.isnull().all(dim) + + if stat == "max": + y = da.fillna(float(da.min() - 1)) + y = y.argmax(dim=dim, skipna=True).where(~mask) + return y + + if stat == "min": + y = da.fillna(float(da.max() + 1)) + y = y.argmin(dim=dim, skipna=True).where(~mask) + return y + + +def _vpos(da): + """ + vPOS = Value at peak of season + """ + return da.max("time") + + +def _pos(da): + """ + POS = DOY of peak of season + """ + return da.isel(time=da.argmax("time")).time.dt.dayofyear + + +def _trough(da): + """ + Trough = Minimum value + """ + return da.min("time") + + +def _aos(vpos, trough): + """ + AOS = Amplitude of season + """ + return vpos - trough + + +def _vsos(da, pos, method_sos="first"): + """ + vSOS = Value at the start of season + Params + ----- + da : xarray.DataArray + method_sos : str, + If 'first' then vSOS is estimated + as the first positive slope on the + greening side of the curve. If 'median', + then vSOS is estimated as the median value + of the postive slopes on the greening side + of the curve. + """ + # select timesteps before peak of season (AKA greening) + greenup = da.where(da.time < pos.time) + # find the first order slopes + green_deriv = greenup.differentiate("time") + # find where the first order slope is postive + pos_green_deriv = green_deriv.where(green_deriv > 0) + # positive slopes on greening side + pos_greenup = greenup.where(~np.isnan(pos_green_deriv)) + # find the median + median = pos_greenup.median("time") + # distance of values from median + distance = pos_greenup - median + + if method_sos == "first": + # find index (argmin) where distance is most negative + idx = allNaN_arg(distance, "time", "min").astype("int16") + + if method_sos == "median": + # find index (argmin) where distance is smallest absolute value + idx = allNaN_arg(np.fabs(distance), "time", "min").astype("int16") + + return pos_greenup.isel(time=idx) + + +def _sos(vsos): + """ + SOS = DOY for start of season + """ + return vsos.time.dt.dayofyear + + +def _veos(da, pos, method_eos="last"): + """ + vEOS = Value at the end of season + Params + ----- + method_eos : str + If 'last' then vEOS is estimated + as the last negative slope on the + senescing side of the curve. If 'median', + then vEOS is estimated as the 'median' value + of the negative slopes on the senescing + side of the curve. + """ + # select timesteps before peak of season (AKA greening) + senesce = da.where(da.time > pos.time) + # find the first order slopes + senesce_deriv = senesce.differentiate("time") + # find where the fst order slope is negative + neg_senesce_deriv = senesce_deriv.where(~np.isnan(senesce_deriv < 0)) + # negative slopes on senescing side + neg_senesce = senesce.where(neg_senesce_deriv) + # find medians + median = neg_senesce.median("time") + # distance to the median + distance = neg_senesce - median + + if method_eos == "last": + # index where last negative slope occurs + idx = allNaN_arg(distance, "time", "min").astype("int16") + + if method_eos == "median": + # index where median occurs + idx = allNaN_arg(np.fabs(distance), "time", "min").astype("int16") + + return neg_senesce.isel(time=idx) + + +def _eos(veos): + """ + EOS = DOY for end of seasonn + """ + return veos.time.dt.dayofyear + + +def _los(da, eos, sos): + """ + LOS = Length of season (in DOY) + """ + los = eos - sos + #handle negative values + los = xr.where( + los >= 0, + los, + da.time.dt.dayofyear.values[-1] + (eos.where(los < 0) - sos.where(los < 0)), + ) + + return los + + +def _rog(vpos, vsos, pos, sos): + """ + ROG = Rate of Greening (Days) + """ + return (vpos - vsos) / (pos - sos) + + +def _ros(veos, vpos, eos, pos): + """ + ROG = Rate of Senescing (Days) + """ + return (veos - vpos) / (eos - pos) + + +def xr_phenology( + da, + stats=[ + "SOS", + "POS", + "EOS", + "Trough", + "vSOS", + "vPOS", + "vEOS", + "LOS", + "AOS", + "ROG", + "ROS", + ], + method_sos="first", + method_eos="last", + verbose=True +): + """ + Obtain land surface phenology metrics from an + xarray.DataArray containing a timeseries of a + vegetation index like NDVI. + + last modified June 2020 + + Parameters + ---------- + da : xarray.DataArray + DataArray should contain a 2D or 3D time series of a + vegetation index like NDVI, EVI + stats : list + list of phenological statistics to return. Regardless of + the metrics returned, all statistics are calculated + due to inter-dependencies between metrics. + Options include: + + * `SOS` = DOY of start of season + * `POS` = DOY of peak of season + * `EOS` = DOY of end of season + * `vSOS` = Value at start of season + * `vPOS` = Value at peak of season + * `vEOS` = Value at end of season + * `Trough` = Minimum value of season + * `LOS` = Length of season (DOY) + * `AOS` = Amplitude of season (in value units) + * `ROG` = Rate of greening + * `ROS` = Rate of senescence + + method_sos : str + If 'first' then vSOS is estimated as the first positive + slope on the greening side of the curve. If 'median', + then vSOS is estimated as the median value of the postive + slopes on the greening side of the curve. + method_eos : str + If 'last' then vEOS is estimated as the last negative slope + on the senescing side of the curve. If 'median', then vEOS is + estimated as the 'median' value of the negative slopes on the + senescing side of the curve. + + Returns + ------- + xarray.Dataset + Dataset containing variables for the selected + phenology statistics + + """ + # Check inputs before running calculations + if dask.is_dask_collection(da): + if version.parse(xr.__version__) < version.parse("0.16.0"): + raise TypeError( + "Dask arrays are not currently supported by this function, " + + "run da.compute() before passing dataArray." + ) + stats_dtype = { + "SOS": np.int16, + "POS": np.int16, + "EOS": np.int16, + "Trough": np.float32, + "vSOS": np.float32, + "vPOS": np.float32, + "vEOS": np.float32, + "LOS": np.int16, + "AOS": np.float32, + "ROG": np.float32, + "ROS": np.float32, + } + da_template = da.isel(time=0).drop("time") + template = xr.Dataset( + { + var_name: da_template.astype(var_dtype) + for var_name, var_dtype in stats_dtype.items() + if var_name in stats + } + ) + da_all_time = da.chunk({"time": -1}) + + lazy_phenology = da_all_time.map_blocks( + xr_phenology, + kwargs=dict( + stats=stats, + method_sos=method_sos, + method_eos=method_eos, + ), + template=xr.Dataset(template), + ) + + try: + crs = da.geobox.crs + lazy_phenology = assign_crs(lazy_phenology, str(crs)) + except: + pass + + return lazy_phenology + + if method_sos not in ("median", "first"): + raise ValueError("method_sos should be either 'median' or 'first'") + + if method_eos not in ("median", "last"): + raise ValueError("method_eos should be either 'median' or 'last'") + + # If stats supplied is not a list, convert to list. + stats = stats if isinstance(stats, list) else [stats] + + # try to grab the crs info + try: + crs = da.geobox.crs + except: + pass + + # remove any remaining all-NaN pixels + mask = da.isnull().all("time") + da = da.where(~mask, other=0) + + # calculate the statistics + if verbose: + print(" Phenology...") + vpos = _vpos(da) + pos = _pos(da) + trough = _trough(da) + aos = _aos(vpos, trough) + vsos = _vsos(da, pos, method_sos=method_sos) + sos = _sos(vsos) + veos = _veos(da, pos, method_eos=method_eos) + eos = _eos(veos) + los = _los(da, eos, sos) + rog = _rog(vpos, vsos, pos, sos) + ros = _ros(veos, vpos, eos, pos) + + # Dictionary containing the statistics + stats_dict = { + "SOS": sos.astype(np.int16), + "EOS": eos.astype(np.int16), + "vSOS": vsos.astype(np.float32), + "vPOS": vpos.astype(np.float32), + "Trough": trough.astype(np.float32), + "POS": pos.astype(np.int16), + "vEOS": veos.astype(np.float32), + "LOS": los.astype(np.int16), + "AOS": aos.astype(np.float32), + "ROG": rog.astype(np.float32), + "ROS": ros.astype(np.float32), + } + + # intialise dataset with first statistic + ds = stats_dict[stats[0]].to_dataset(name=stats[0]) + + # add the other stats to the dataset + for stat in stats[1:]: + if verbose: + print(" " + stat) + stats_keep = stats_dict.get(stat) + ds[stat] = stats_dict[stat] + + try: + ds = assign_crs(ds, str(crs)) + except: + pass + + return ds.drop("time") + + +def temporal_statistics(da, stats): + """ + Calculate various generic summary statistics on any timeseries. + + This function uses the hdstats temporal library: + https://github.com/daleroberts/hdstats/blob/master/hdstats/ts.pyx + + last modified June 2020 + + Parameters + ---------- + da : xarray.DataArray + DataArray should contain a 3D time series. + stats : list + list of temporal statistics to calculate. + Options include: + + * 'discordance' = + * 'f_std' = std of discrete fourier transform coefficients, returns + three layers: f_std_n1, f_std_n2, f_std_n3 + * 'f_mean' = mean of discrete fourier transform coefficients, returns + three layers: f_mean_n1, f_mean_n2, f_mean_n3 + * 'f_median' = median of discrete fourier transform coefficients, returns + three layers: f_median_n1, f_median_n2, f_median_n3 + * 'mean_change' = mean of discrete difference along time dimension + * 'median_change' = median of discrete difference along time dimension + * 'abs_change' = mean of absolute discrete difference along time dimension + * 'complexity' = + * 'central_diff' = + * 'num_peaks' : The number of peaks in the timeseries, defined with a local + window of size 10. NOTE: This statistic is very slow + + Returns + ------- + xarray.Dataset + Dataset containing variables for the selected + temporal statistics + + """ + + # if dask arrays then map the blocks + if dask.is_dask_collection(da): + if version.parse(xr.__version__) < version.parse("0.16.0"): + raise TypeError( + "Dask arrays are only supported by this function if using, " + + "xarray v0.16, run da.compute() before passing dataArray." + ) + + # create a template that matches the final datasets dims & vars + arr = da.isel(time=0).drop("time") + + # deal with the case where fourier is first in the list + if stats[0] in ("f_std", "f_median", "f_mean"): + template = xr.zeros_like(arr).to_dataset(name=stats[0] + "_n1") + template[stats[0] + "_n2"] = xr.zeros_like(arr) + template[stats[0] + "_n3"] = xr.zeros_like(arr) + + for stat in stats[1:]: + if stat in ("f_std", "f_median", "f_mean"): + template[stat + "_n1"] = xr.zeros_like(arr) + template[stat + "_n2"] = xr.zeros_like(arr) + template[stat + "_n3"] = xr.zeros_like(arr) + else: + template[stat] = xr.zeros_like(arr) + else: + template = xr.zeros_like(arr).to_dataset(name=stats[0]) + + for stat in stats: + if stat in ("f_std", "f_median", "f_mean"): + template[stat + "_n1"] = xr.zeros_like(arr) + template[stat + "_n2"] = xr.zeros_like(arr) + template[stat + "_n3"] = xr.zeros_like(arr) + else: + template[stat] = xr.zeros_like(arr) + try: + template = template.drop("spatial_ref") + except: + pass + + # ensure the time chunk is set to -1 + da_all_time = da.chunk({"time": -1}) + + # apply function across chunks + lazy_ds = da_all_time.map_blocks( + temporal_statistics, kwargs={"stats": stats}, template=template + ) + + try: + crs = da.geobox.crs + lazy_ds = assign_crs(lazy_ds, str(crs)) + except: + pass + + return lazy_ds + + # If stats supplied is not a list, convert to list. + stats = stats if isinstance(stats, list) else [stats] + + # grab all the attributes of the xarray + x, y, time, attrs = da.x, da.y, da.time, da.attrs + + # deal with any all-NaN pixels by filling with 0's + mask = da.isnull().all("time") + da = da.where(~mask, other=0) + + # ensure dim order is correct for functions + da = da.transpose("y", "x", "time").values + + stats_dict = { + "discordance": lambda da: hdstats.discordance(da, n=10), + "f_std": lambda da: hdstats.fourier_std(da, n=3, step=5), + "f_mean": lambda da: hdstats.fourier_mean(da, n=3, step=5), + "f_median": lambda da: hdstats.fourier_median(da, n=3, step=5), + "mean_change": lambda da: hdstats.mean_change(da), + "median_change": lambda da: hdstats.median_change(da), + "abs_change": lambda da: hdstats.mean_abs_change(da), + "complexity": lambda da: hdstats.complexity(da), + "central_diff": lambda da: hdstats.mean_central_diff(da), + "num_peaks": lambda da: hdstats.number_peaks(da, 10), + } + + print(" Statistics:") + # if one of the fourier functions is first (or only) + # stat in the list then we need to deal with this + if stats[0] in ("f_std", "f_median", "f_mean"): + print(" " + stats[0]) + stat_func = stats_dict.get(str(stats[0])) + zz = stat_func(da) + n1 = zz[:, :, 0] + n2 = zz[:, :, 1] + n3 = zz[:, :, 2] + + # intialise dataset with first statistic + ds = xr.DataArray( + n1, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"] + ).to_dataset(name=stats[0] + "_n1") + + # add other datasets + for i, j in zip([n2, n3], ["n2", "n3"]): + ds[stats[0] + "_" + j] = xr.DataArray( + i, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"] + ) + else: + # simpler if first function isn't fourier transform + first_func = stats_dict.get(str(stats[0])) + print(" " + stats[0]) + ds = first_func(da) + + # convert back to xarray dataset + ds = xr.DataArray( + ds, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"] + ).to_dataset(name=stats[0]) + + # loop through the other functions + for stat in stats[1:]: + print(" " + stat) + + # handle the fourier transform examples + if stat in ("f_std", "f_median", "f_mean"): + stat_func = stats_dict.get(str(stat)) + zz = stat_func(da) + n1 = zz[:, :, 0] + n2 = zz[:, :, 1] + n3 = zz[:, :, 2] + + for i, j in zip([n1, n2, n3], ["n1", "n2", "n3"]): + ds[stat + "_" + j] = xr.DataArray( + i, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"] + ) + + else: + # Select a stats function from the dictionary + # and add to the dataset + stat_func = stats_dict.get(str(stat)) + ds[stat] = xr.DataArray( + stat_func(da), attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"] + ) + + # try to add back the geobox + try: + crs = da.geobox.crs + ds = assign_crs(ds, str(crs)) + except: + pass + + return ds diff --git a/deafrica_tools/untitled.txt b/deafrica_tools/untitled.txt new file mode 100644 index 0000000..e69de29 diff --git a/deafrica_tools/wetlands.py b/deafrica_tools/wetlands.py new file mode 100644 index 0000000..4d7377a --- /dev/null +++ b/deafrica_tools/wetlands.py @@ -0,0 +1,732 @@ +""" +Functions for working with the Wetlands Insight Tool (WIT) +""" + +# Import required packages + +# Force GeoPandas to use Shapely instead of PyGEOS +# In a future release, GeoPandas will switch to using Shapely by default. +import os +os.environ['USE_PYGEOS'] = '0' + +import warnings +import numpy as np +import pandas as pd +import geopandas as gpd +import seaborn as sns +import xarray as xr +import matplotlib.pyplot as plt +from skimage import exposure +import matplotlib.animation as animation +import matplotlib.patheffects as PathEffects +from mpl_toolkits.axes_grid1.inset_locator import inset_axes +from dask.distributed import progress + +import datacube +from datacube.utils import masking +from datacube.utils import geometry + +from deafrica_tools.bandindices import calculate_indices +from deafrica_tools.datahandling import load_ard, wofs_fuser +from deafrica_tools.spatial import xr_rasterize +from deafrica_tools.classification import HiddenPrints + + +def WIT_drill( + gdf, + time, + min_gooddata=0.85, + TCW_threshold=-0.035, + resample_frequency=None, + export_csv=None, + dask_chunks=None, + verbose=False, + verbose_progress=False, +): + """ + The Wetlands Insight Tool run onver an extent covered by a polygon. + This function loads FC, WOfS, and Landsat data, and calculates tasseled + cap wetness, in order to determine the dominant land cover class + within a polygon at each satellite observation. + + The output is a pandas dataframe containing a timeseries of the relative + fractions of each class at each time-step. This forms the input to produce + a stacked line-plot. + + Last modified: Oct 2021 + + Parameters + ---------- + gdf : geopandas.GeoDataFrame + The dataframe must only contain a single row, + containing the polygon you wish to interrograte. + time : tuple + a tuple containing the time range over which to run the WIT. + e.g. ('2015-01' , '2019-12') + min_gooddata : Float, optional + A number between 0 and 1 (e.g 0.8) indicating the minimum percentage + of good quality pixels required for a satellite observation to be loaded + and therefore included in the WIT plot. This number should, at a minimum, + be set to 0.80 to limit biases in the result if not resampling the time-series. + If resampling the data using the parameter `resample_frequency`, then + setting this number to 0 (or a low float number) is acceptable. + TCW_threshold : Int, optional + The tasseled cap wetness threshold, beyond which a pixel will be + considered 'wet'. Defaults to -0.035. + resample_frequency : str + Option for resampling time-series of input datasets. This option is useful + for either smoothing the WIT plot, or because the area of analysis is larger + than a scene width and therefore requires composites. Options include any + str accepted by `xarray.resample(time=)`. The resampling method used is .max() + export_csv : str, optional + To save the returned pandas dataframe as a .csv file, pass a + a location string (e.g. 'output/results.csv') + dask_chunks : dict, optional + To lazily load the datasets using dask, pass a dictionary containing + the dimensions over which to chunk e.g. {'time':-1, 'x':250, 'y':250}. + verbose: bool, optional + If true, print statements are putput detailing the progress of the tool. + verbose_progress: bool, optional + For use with Dask progress bar + + Returns + ------- + df : Pandas.Dataframe + A pandas dataframe containing the timeseries of relative fractions + of each land cover class (WOfs, FC, TCW) + + """ + # add geom to dc query dict + if isinstance(gdf, datacube.utils.geometry._base.Geometry): + gdf = gpd.GeoDataFrame({'col1':['name'],'geometry':gdf.geom}, crs=gdf.crs) + geom = geometry.Geometry(geom=gdf.iloc[0].geometry, crs=gdf.crs) + query = {"geopolygon": geom, "time": time} + + # Create a datacube instance + dc = datacube.Datacube(app="wetlands insight tool") + + # load landsat 5,7,8 data + warnings.filterwarnings("ignore") + + if verbose_progress: + print("Loading Landsat data") + ds_ls = load_ard( + dc=dc, + products=["ls8_sr", "ls7_sr", "ls5_sr"], + output_crs="epsg:6933", + min_gooddata=min_gooddata, + mask_filters=(['opening', 3], ['dilation', 3]), + measurements=["red", "green", "blue", "nir", "swir_1", "swir_2"], + dask_chunks=dask_chunks, + group_by="solar_day", + resolution=(-30, 30), + verbose=verbose, + **query, + ) + + # create polygon mask + mask = xr_rasterize(gdf.iloc[[0]], ds_ls) + ds_ls = ds_ls.where(mask) + + # calculate tasselled cap wetness within masked AOI + if verbose: + print("calculating tasseled cap wetness index ") + + with HiddenPrints(): #suppres the prints from this func + tcw = calculate_indices( + ds_ls, index=["TCW"], normalise=False, satellite_mission="ls", drop=True + ) + + if resample_frequency is not None: + if verbose: + print('Resampling TCW to '+ resample_frequency) + tcw = tcw.resample(time=resample_frequency).max() + + tcw = tcw.TCW >= TCW_threshold + tcw = tcw.where(mask, 0) + tcw = tcw.persist() + + if verbose: + print("Loading WOfS layers ") + + wofls = dc.load( + product="wofs_ls", + like=ds_ls, + fuse_func=wofs_fuser, + dask_chunks=dask_chunks, + collection_category="T1", + ) + + # boolean of wet/dry + wofls_wet = masking.make_mask(wofls.water, wet=True) + + if resample_frequency is not None: + if verbose: + print('Resampling WOfS to '+ resample_frequency) + wofls_wet = wofls_wet.resample(time=resample_frequency).max() + + # mask sure wofs matches other datasets + wofls_wet = wofls_wet.where(wofls_wet.time == tcw.time) + + # apply the polygon mask + wofls_wet = wofls_wet.where(mask) + + # load Fractional cover + if verbose: + print("Loading fractional Cover") + + # load fractional cover + fc_ds = dc.load( + product="fc_ls", + time=time, + dask_chunks=dask_chunks, + like=ds_ls, + measurements=["pv", "npv", "bs"], + collection_category="T1", + ) + + # use wofls mask to cloud mask FC + clear_and_dry = masking.make_mask(wofls, dry=True).water + fc_ds = fc_ds.where(clear_and_dry) + + if resample_frequency is not None: + if verbose: + print('Resampling FC to '+ resample_frequency) + fc_ds = fc_ds.resample(time=resample_frequency).max() + + # mask sure fc matches other datasets + fc_ds = fc_ds.where(fc_ds.time == tcw.time) + + # mask with polygon + fc_ds = fc_ds.where(mask) + + # mask with TC wetness + fc_ds_noTCW = fc_ds.where(tcw == False) + + if verbose: + print("Generating classification") + + # Cast the dataset to a dataarray + fc_ds_noTCW = fc_ds_noTCW.to_array(dim="variable", name="fc_ds_noTCW") + + # turn FC array into integer only as nanargmax doesn't + # seem to handle floats the way we want it to + fc_int = fc_ds_noTCW.astype("int8") + + # use nanargmax to get the index of the maximum value + BSPVNPV = fc_int.argmax(dim="variable") + + #int dytype remocves NaNs so we need to create mask again + FC_mask = np.isfinite(fc_ds_noTCW).all(dim="variable") + BSPVNPV = BSPVNPV.where(FC_mask) + + # Restack the Fractional cover dataset all together + # CAUTION:ARGMAX DEPENDS ON ORDER OF VARIABALES IN + # DATASET. NEED TO ADJUST BELOW DEPENDING ON ORDER OF FC VARIABLES + + FC_dominant = xr.Dataset( + { + "bs": (BSPVNPV == 2).where(FC_mask), + "pv": (BSPVNPV == 0).where(FC_mask), + "npv": (BSPVNPV == 1).where(FC_mask), + } + ) + + # pixel counts + pixels = mask.sum(dim=["x", "y"]) + + + if verbose_progress: + print("Computing wetness") + tcw_pixel_count = tcw.sum(dim=["x", "y"]).compute() + + if verbose_progress: + print("Computing green veg, dry veg, and bare soil") + FC_count = FC_dominant.sum(dim=["x", "y"]).compute() + + if verbose_progress: + print("Computing open water") + wofs_pixels = wofls_wet.sum(dim=["x", "y"]).compute() + + # count percentages + wofs_area_percent = (wofs_pixels / pixels) * 100 + tcw_area_percent = (tcw_pixel_count / pixels) * 100 + tcw_less_wofs = tcw_area_percent - wofs_area_percent # wet not wofs + + # Fractional cover pixel count method + # Get number of FC pixels, divide by total number of pixels per polygon + # Work out the number of nodata pixels in the data + BS_percent = (FC_count.bs / pixels) * 100 + PV_percent = (FC_count.pv / pixels) * 100 + NPV_percent = (FC_count.npv / pixels) * 100 + NoData_count = (( + 100 - wofs_area_percent - tcw_less_wofs - PV_percent - NPV_percent - BS_percent + ) / 100) * pixels + + # re-do percentages but now handling any no-data pixels within polygon + BS_percent = (FC_count.bs / (pixels - NoData_count)) * 100 + PV_percent = (FC_count.pv / (pixels - NoData_count)) * 100 + NPV_percent = (FC_count.npv / (pixels - NoData_count)) * 100 + wofs_area_percent = (wofs_pixels / (pixels - NoData_count)) * 100 + tcw_area_percent = (tcw_pixel_count / (pixels - NoData_count)) * 100 + tcw_less_wofs = tcw_area_percent - wofs_area_percent + + # Sometimes when we resample datastes, WOfS extent can be + # greater than the wetness extent, thus make negative values == zero + tcw_less_wofs = tcw_less_wofs.where(tcw_less_wofs>=0, 0) + + # start setup of dataframe by adding only one dataset + df = pd.DataFrame( + data=wofs_area_percent.data, + index=wofs_area_percent.time.values, + columns=["wofs_area_percent"], + ) + + # add data into pandas dataframe for export + df["wet_percent"] = tcw_less_wofs.data + df["green_veg_percent"] = PV_percent.data + df["dry_veg_percent"] = NPV_percent.data + df["bare_soil_percent"] = BS_percent.data + + # round numbers + df = df.round(2) + + # save the csv of the output data used to create the stacked plot for the polygon drill + if export_csv: + if verbose: + print("exporting csv: " + export_csv) + df.to_csv(export_csv, index_label="Datetime") + + return df + + +def animated_timeseries_WIT( + ds, + df, + output_path, + width_pixels=1000, + interval=200, + bands=["red", "green", "blue"], + percentile_stretch=(0.02, 0.98), + image_proc_func=None, + title=False, + show_date=True, + annotation_kwargs={}, + onebandplot_cbar=True, + onebandplot_kwargs={}, + shapefile_path=None, + shapefile_kwargs={}, + pandasplot_kwargs={}, + time_dim="time", + x_dim="x", + y_dim="y", +): + + ############### + # Setup steps # + ############### + + # Test if all dimensions exist in dataset + if time_dim in ds and x_dim in ds and y_dim in ds: + + # Test if there is one or three bands, and that all exist in both datasets: + if ((len(bands) == 3) | (len(bands) == 1)) & all( + [(b in ds.data_vars) for b in bands] + ): + + # Import xarrays as lists of three band numpy arrays + imagelist, vmin, vmax = _ds_to_arrraylist( + ds, + bands=bands, + time_dim=time_dim, + x_dim=x_dim, + y_dim=y_dim, + percentile_stretch=percentile_stretch, + image_proc_func=image_proc_func, + ) + + # Get time, x and y dimensions of dataset and calculate width vs height of plot + timesteps = len(ds[time_dim]) + width = len(ds[x_dim]) + height = len(ds[y_dim]) + width_ratio = float(width) / float(height) + height = 10.0 / width_ratio + + # If title is supplied as a string, multiply out to a list with one string per timestep. + # Otherwise, use supplied list for plot titles. + if isinstance(title, str) or isinstance(title, bool): + title_list = [title] * timesteps + else: + title_list = title + + # Set up annotation parameters that plt.imshow plotting for single band array images. + # The nested dict structure sets default values which can be overwritten/customised by the + # manually specified `onebandplot_kwargs` + onebandplot_kwargs = dict( + { + "cmap": "Greys", + "interpolation": "bilinear", + "vmin": vmin, + "vmax": vmax, + "tick_colour": "black", + "tick_fontsize": 11, + }, + **onebandplot_kwargs, + ) + + # Use pop to remove the two special tick kwargs from the onebandplot_kwargs dict, and save individually + onebandplot_tick_colour = onebandplot_kwargs.pop("tick_colour") + onebandplot_tick_fontsize = onebandplot_kwargs.pop("tick_fontsize") + + # Set up annotation parameters that control font etc. The nested dict structure sets default + # values which can be overwritten/customised by the manually specified `annotation_kwargs` + annotation_kwargs = dict( + { + "xy": (1, 1), + "xycoords": "axes fraction", + "xytext": (-5, -5), + "textcoords": "offset points", + "horizontalalignment": "right", + "verticalalignment": "top", + "fontsize": 15, + "color": "white", + "path_effects": [ + PathEffects.withStroke(linewidth=3, foreground="black") + ], + }, + **annotation_kwargs, + ) + + # Define default plotting parameters for the overlaying shapefile(s). The nested dict structure sets + # default values which can be overwritten/customised by the manually specified `shapefile_kwargs` + shapefile_kwargs = dict( + {"linewidth": 2, "edgecolor": "black", "facecolor": "#00000000"}, + **shapefile_kwargs, + ) + + # Define default plotting parameters for the right-hand line plot. The nested dict structure sets + # default values which can be overwritten/customised by the manually specified `pandasplot_kwargs` + pandasplot_kwargs = dict({}, **pandasplot_kwargs) + + ################### + # Initialise plot # + ################### + + # Set up figure + fig, (ax1, ax2) = plt.subplots( + ncols=2, gridspec_kw={"width_ratios": [1, 2]} + ) + fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0.2, hspace=0) + fig.set_size_inches(10.0, height * 0.5, forward=True) + ax1.axis("off") + ax2.margins(x=0.01) + ax2.xaxis.label.set_visible(False) + + # Initialise axesimage objects to be updated during animation, setting extent from dims + extents = [ + float(ds[x_dim].min()), + float(ds[x_dim].max()), + float(ds[y_dim].min()), + float(ds[y_dim].max()), + ] + im = ax1.imshow(imagelist[0], extent=extents, **onebandplot_kwargs) + + # Initialise right panel and set y axis limits + # set up color palette + pal = [ + sns.xkcd_rgb["cobalt blue"], + sns.xkcd_rgb["neon blue"], + sns.xkcd_rgb["grass"], + sns.xkcd_rgb["beige"], + sns.xkcd_rgb["brown"], + ] + + # make a stacked area plot + ax2.stackplot( + df.index, + df.wofs_area_percent, + df.wet_percent, + df.green_veg_percent, + df.dry_veg_percent, + df.bare_soil_percent, + labels=["open water", "wet", "green veg", "dry veg", "bare soil"], + colors=pal, + alpha=0.6, + **pandasplot_kwargs, + ) + + ax2.legend(loc="lower left", framealpha=0.6) + + df1 = pd.DataFrame( + { + "wofs_area_percent": df.wofs_area_percent, + "wet_percent": df.wofs_area_percent + df.wet_percent, + "green_veg_percent": df.wofs_area_percent + + df.wet_percent + + df.green_veg_percent, + "dry_veg_percent": df.wofs_area_percent + + df.wet_percent + + df.green_veg_percent + + df.dry_veg_percent, + "bare_soil_percent": df.dry_veg_percent + + df.green_veg_percent + + df.wofs_area_percent + + df.wet_percent + + df.bare_soil_percent, + } + ) + df1 = df1.set_index(df.index) + + line_test = df1.plot( + ax=ax2, legend=False, color="black", **pandasplot_kwargs + ) + + # set axis limits to the min and max + ax2.set(xlim=(df.index[0], df.index[-1]), ylim=(0, 100)) + + # add a legend and a tight plot box + + ax2.set_title("Fractional Cover, Wetness, and Water") + + # Initialise annotation objects to be updated during animation + t = ax1.annotate("", **annotation_kwargs) + + ######################### + # Add optional overlays # + ######################### + + # Optionally add shapefile overlay(s) from either string path or list of string paths + if isinstance(shapefile_path, str): + + shapefile = gpd.read_file(shapefile_path) + shapefile.plot(**shapefile_kwargs, ax=ax1) + + elif isinstance(shapefile_path, list): + + # Iterate through list of string paths + for shapefile in shapefile_path: + + shapefile = gpd.read_file(shapefile) + shapefile.plot(**shapefile_kwargs, ax=ax1) + + # After adding shapefile, fix extents of plot + ax1.set_xlim(extents[0], extents[1]) + ax1.set_ylim(extents[2], extents[3]) + + # Optionally add colourbar for one band images + if (len(bands) == 1) & onebandplot_cbar: + _add_colourbar( + ax1, + im, + tick_fontsize=onebandplot_tick_fontsize, + tick_colour=onebandplot_tick_colour, + vmin=onebandplot_kwargs["vmin"], + vmax=onebandplot_kwargs["vmax"], + ) + + ######################################## + # Create function to update each frame # + ######################################## + + # Function to update figure + + def update_figure(frame_i): + + #################### + # Plot image panel # + #################### + + # If possible, extract dates from time dimension + try: + + # Get human-readable date info (e.g. "16 May 1990") + ts = ds[time_dim][{time_dim: frame_i}].dt + year = ts.year.item() + month = ts.month.item() + day = ts.day.item() + date_string = "{} {} {}".format( + day, calendar.month_abbr[month], year + ) + + except: + + date_string = ds[time_dim][{time_dim: frame_i}].values.item() + + # Create annotation string based on title and date specifications: + title = title_list[frame_i] + if title and show_date: + title_date = "{}\n{}".format(date_string, title) + elif title and not show_date: + title_date = "{}".format(title) + elif show_date and not title: + title_date = "{}".format(date_string) + else: + title_date = "" + + # Update left panel with annotation and image + im.set_array(imagelist[frame_i]) + t.set_text(title_date) + + ######################## + # Plot linegraph panel # + ######################## + + # Create list of artists to return + artist_list = [im, t] + + # Update right panel with temporal line subset, adding each new line into artist_list + for i, line in enumerate(line_test.lines): + + # Clip line data to current time, and get x and y values + y = df1[ + df1.index + <= datetime(year=year, month=month, day=day, hour=23, minute=59) + ].iloc[:, i] + x = df1[ + df1.index + <= datetime(year=year, month=month, day=day, hour=23, minute=59) + ].index + + # Plot lines after stripping NaNs (this produces continuous, unbroken lines) + line.set_data(x[y.notnull()], y[y.notnull()]) + artist_list.extend([line]) + + # Return the artists set + return artist_list + + # Nicely space subplots + fig.tight_layout() + + ############################## + # Generate and run animation # + ############################## + + # Generate animation + ani = animation.FuncAnimation( + fig=fig, + func=update_figure, + frames=timesteps, + interval=interval, + blit=True, + ) + + # Export as either MP4 or GIF + if output_path[-3:] == "mp4": + print(" Exporting animation to {}".format(output_path)) + ani.save(output_path, dpi=width_pixels / 10.0) + + elif output_path[-3:] == "wmv": + print(" Exporting animation to {}".format(output_path)) + ani.save( + output_path, + dpi=width_pixels / 10.0, + writer=animation.FFMpegFileWriter( + fps=1000 / interval, bitrate=4000, codec="wmv2" + ), + ) + + elif output_path[-3:] == "gif": + print(" Exporting animation to {}".format(output_path)) + ani.save(output_path, dpi=width_pixels / 10.0, writer="imagemagick") + + else: + print(" Output file type must be either .mp4, .wmv or .gif") + + else: + print( + "Please select either one or three bands that all exist in the input dataset" + ) + + else: + print( + "At least one x, y or time dimension does not exist in the input dataset. Please use the `time_dim`," + "`x_dim` or `y_dim` parameters to override the default dimension names used for plotting" + ) + + +# Define function to convert xarray dataset to list of one or three band numpy arrays + + +def _ds_to_arrraylist( + ds, bands, time_dim, x_dim, y_dim, percentile_stretch, image_proc_func=None +): + """ + Converts an xarray dataset to a list of numpy arrays for plt.imshow plotting + """ + + # Compute percents + p_low, p_high = ds[bands].to_array().quantile(percentile_stretch).values + + array_list = [] + for i, timestep in enumerate(ds[time_dim]): + + # Select single timestep from the data array + ds_i = ds[{time_dim: i}] + + # Get shape of array + x = len(ds[x_dim]) + y = len(ds[y_dim]) + + if len(bands) == 1: + + # Create new one band array + img_toshow = exposure.rescale_intensity( + ds_i[bands[0]].values, in_range=(p_low, p_high), out_range="image" + ) + + else: + + # Create new three band array + rawimg = np.zeros((y, x, 3), dtype=np.float32) + + # Add xarray bands into three dimensional numpy array + for band, colour in enumerate(bands): + + rawimg[:, :, band] = ds_i[colour].values + + # Stretch contrast using percentile values + img_toshow = exposure.rescale_intensity( + rawimg, in_range=(p_low, p_high), out_range=(0, 1.0) + ) + + # Optionally image processing + if image_proc_func: + + img_toshow = image_proc_func(img_toshow).clip(0, 1) + + array_list.append(img_toshow) + + return array_list, p_low, p_high + + +def _add_colourbar( + ax, im, vmin, vmax, cmap="Greys", tick_fontsize=15, tick_colour="black" +): + """ + Add a nicely formatted colourbar to an animation panel + """ + + # Add colourbar + axins2 = inset_axes(ax, width="97%", height="4%", loc=8, borderpad=1) + plt.gcf().colorbar( + im, cax=axins2, orientation="horizontal", ticks=np.linspace(vmin, vmax, 3) + ) + axins2.xaxis.set_ticks_position("top") + axins2.tick_params(axis="x", colors=tick_colour, labelsize=tick_fontsize) + + # Justify left and right labels to edge of plot + axins2.get_xticklabels()[0].set_horizontalalignment("left") + axins2.get_xticklabels()[-1].set_horizontalalignment("right") + labels = [item.get_text() for item in axins2.get_xticklabels()] + labels[0] = " " + labels[0] + labels[-1] = labels[-1] + " " + + +if __name__ == "__main__": + # print that we are running the testing + print("Testing..") + # import doctest to test our module for documentation + import doctest + + doctest.testmod() + print("Testing done") diff --git a/new_import.py b/new_import.py new file mode 100644 index 0000000..a49a430 --- /dev/null +++ b/new_import.py @@ -0,0 +1,320 @@ +import matplotlib.pyplot as plt + +# Common imports and settings +import os, sys +os.environ['USE_PYGEOS'] = '0' +from IPython.display import Markdown +import pandas as pd +pd.set_option("display.max_rows", None) +import xarray as xr + +# Datacube +import datacube +from datacube.utils.rio import configure_s3_access +from datacube.utils import masking +from datacube.utils.cog import write_cog +# https://github.com/GeoscienceAustralia/dea-notebooks/tree/develop/Tools +from dea_tools.plotting import display_map, rgb +from dea_tools.datahandling import mostcommon_crs + +# EASI defaults +easinotebooksrepo = '/home/jovyan/easi-notebooks' +if easinotebooksrepo not in sys.path: sys.path.append(easinotebooksrepo) +from easi_tools import EasiDefaults, xarray_object_size, notebook_utils, unset_cachingproxy +from easi_tools.load_s2l2a import load_s2l2a_with_offset +from dask.distributed import progress + +# Data tools +import numpy as np +from datetime import datetime + +# Datacube +from datacube.utils import masking # https://github.com/opendatacube/datacube-core/blob/develop/datacube/utils/masking.py +from odc.algo import enum_to_bool # https://github.com/opendatacube/odc-algo/blob/main/odc/algo/_masking.py +from odc.algo import xr_reproject # https://github.com/opendatacube/odc-algo/blob/main/odc/algo/_warp.py +from datacube.utils.geometry import GeoBox, box # https://github.com/opendatacube/datacube-core/blob/develop/datacube/utils/geometry/_base.py + +# Holoviews, Datashader and Bokeh +import hvplot.pandas +import hvplot.xarray +import holoviews as hv +import panel as pn +import colorcet as cc +import cartopy.crs as ccrs +from datashader import reductions +from holoviews import opts +from utils import load_data_geo +import rasterio +import rioxarray +# import geoviews as gv +# from holoviews.operation.datashader import rasterize +hv.extension('bokeh', logo=False) + +from deafrica_tools.bandindices import calculate_indices +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score, classification_report +from sklearn.preprocessing import LabelEncoder + +from sklearn.pipeline import Pipeline +from sklearn.ensemble import RandomForestClassifier +from sklearn.impute import SimpleImputer +from sklearn.preprocessing import StandardScaler +from sklearn.model_selection import GridSearchCV +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score +from shapely.geometry import Point, Polygon +import geopandas as gpd +from pyproj import CRS +from matplotlib.colors import ListedColormap +from holoviews import opts +from datashader import reductions +from bokeh.models.tickers import FixedTicker +from rioxarray.merge import merge_arrays + +import joblib + + +def load_data(dc, date_range, longtitude_range, latitude_range): + product = 's2_l2a' + query = { + 'product': product, # Product name + 'x': longtitude_range, # "x" axis bounds + 'y': latitude_range, # "y" axis bounds + 'time': date_range, # Any parsable date strings + } + native_crs = notebook_utils.mostcommon_crs(dc, query) + print(f'Most common native CRS: {native_crs}') + measurements = ['blue', 'green', 'red', 'nir', 'scl'] + + load_params = { + 'measurements': measurements, # Selected measurement or alias names + 'output_crs': native_crs, # Target EPSG code + 'resolution': (-10, 10), # Target resolution + 'group_by': 'solar_day', # Scene grouping + 'dask_chunks': {'x': 2048, 'y': 2048}, # Dask chunks + } + data = load_s2l2a_with_offset( + dc, + query | load_params # Combine the two dicts that contain our search and load parameters + ) + return data + + +def mask_clean(data): + flag_name = 'scl' + flag_desc = masking.describe_variable_flags(data[flag_name]) # Pandas dataframe + display(flag_desc) + display(flag_desc.loc['qa'].values[1]) + # Create a "data quality" Mask layer + flags_def = flag_desc.loc['qa'].values[1] + good_pixel_flags = [flags_def[str(i)] for i in [2, 4, 5, 6]] # To pass strings to enum_to_bool() + + # enum_to_bool calculates the pixel-wise "or" of each set of pixels given by good_pixel_flags + # 1 = good data + # 0 = "bad" data + good_pixel_mask = enum_to_bool(data[flag_name], good_pixel_flags) + data_layer_names = [x for x in data.data_vars if x != 'scl'] + # Apply good pixel mask to blue, green, red and nir. + result = data[data_layer_names].where(good_pixel_mask).persist() + return result + + +def fill_nan(ndvi, time_split): + rs = [] + for times in time_split: + tmp = ndvi.sel(time=times) + fill_ds = tmp.sel(time=times).bfill(dim='time') + fill_ds = fill_ds.sel(time=times).ffill(dim='time') + rs.append(fill_ds) + merged_ndvi = xr.concat([i for i in rs], dim="time") + fill_m = merged_ndvi.bfill(dim="time") + fill_m = fill_m.ffill(dim="time") + return fill_m + + +def load_train_data(train_path): + train = load_data_geo(train_path) + return train + + +def load_sen1(name_vh, name_vv): + dsvv = rioxarray.open_rasterio(name_vv) + dsvh = rioxarray.open_rasterio(name_vh) + return dsvh, dsvv + + +def get_data_sen1_and_sen2(train, average_ndvi, dsvh, dsvv): + loaded_datasets = {} + for idx, point in train.iterrows(): + key = f"point_{idx + 1}" + try: + ndvi_data = average_ndvi.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values + vh_data = dsvh.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values + vv_data = dsvv.sel(x=point.geometry.x, y=point.geometry.y, method='nearest').values + loaded_datasets[key] = { + "data": np.concatenate((ndvi_data, vh_data, vv_data)), + "label": point.HT_code + } + except Exception as e: + # loaded_datasets[key] = None + print(e) + return loaded_datasets + + +def split_train_data(train, label_mapping, datasets): + label_encoder = LabelEncoder() + + # Fit and transform the labels + labels = train.Hientrang.values + numeric_labels = label_encoder.fit_transform([label_mapping[label] for label in labels]) + X = [] + x_new = [] + lb_new = [] + for k, v in datasets.items(): + X.append(v) + for i in range(len(X)): + if X[i] is not None: + x_new.append(X[i]["data"]) + lb_new.append(numeric_labels[i]) + X_train, X_temp, y_train, y_temp= train_test_split(x_new, lb_new, test_size=0.4, random_state=42) + X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42) + return X_train, X_val, X_test, y_train, y_val, y_test + + +def train_with_rf(X_train, X_val, y_train, y_val): + # Takes 1-2 minutes to complete + + # Tạo RandomForestClassifier mặc định để sử dụng làm mô hình ban đầu trong pipeline + base_model = RandomForestClassifier(random_state=42, n_jobs=-1) + + # Tạo pipeline + pipeline = Pipeline([ + # ('imputer', SimpleImputer(strategy='mean')), + ('scaler', StandardScaler()), + ('classifier', base_model), + ]) + # Thiết lập các tham số bạn muốn tối ưu hóa + param_grid = { + 'classifier__n_estimators': [100, 300, 500, 700, 1000], + 'classifier__max_depth': [6, 8, 10, 15, 20], + 'classifier__criterion': ['gini', 'entropy'], + } + + # Sử dụng GridSearchCV để tìm bộ tham số tốt nhất + grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy', n_jobs=-1) + grid_search.fit(X_train, y_train) + + # In ra bộ tham số tốt nhất + best_params = grid_search.best_params_ + print("Best Parameters:", best_params) + + # Dự đoán trên tập kiểm tra + y_pred = grid_search.predict(X_val) + + # Đánh giá kết quả + accuracy = accuracy_score(y_val, y_pred) + print(f"Accuracy: {round(accuracy, 2)*100} %") + return grid_search + + +def save_model(name_file, grid_search): + dir_save_model = "model_train" + if not os.path.exists(dir_save_model): + os.mkdir(dir_save_model) + joblib.dump(grid_search, os.path.join(dir_save_model, name_file)) + print("Done!") + + +def predict(model, data_crs, ndvi, vh, vv): + data_predict = [] + for i in range(ndvi.shape[1]): + ndvi_tmp = ndvi.isel(y=i).values + vh_data = vh.sel(y=ndvi.y.values[i], method='nearest').values + vv_data = vv.sel(y=ndvi.y.values[i], method='nearest').values + all_tmp = np.concatenate((ndvi_tmp, vh_data, vv_data), axis=0) + data_predict.extend(all_tmp.T) + y_pred = model.predict(data_predict) + final_label = y_pred.reshape(ndvi.y.shape[0], ndvi.x.shape[0]) + + final_xarray_save = xr.DataArray(final_label, dims=("y", "x")) + final_xarray_save = final_xarray_save.rio.write_crs(data_crs) + + x_values = ndvi.x.values + y_values = ndvi.y.values + + data_array = xr.DataArray(final_xarray_save, + coords={'x': x_values, 'y': y_values}, + dims=['y', 'x']) + data_array = data_array.rio.write_crs(ndvi.rio.crs) + return data_array + + +def cut_according_shp(thuanhoa_path, average_ndvi, data_array): + gdf = gpd.read_file(thuanhoa_path) + gdf = gdf.to_crs(average_ndvi.rio.crs) + polygon_coords = list(gdf.geometry.values[0].exterior.coords) + polygon_coordinates = [(x, y) for x, y in polygon_coords] + + geometries = [ + { + 'type': 'Polygon', + 'coordinates': [polygon_coordinates] + } + ] + region_result = data_array.rio.clip(geometries, data_array.rio.crs, drop=False) + region_result = region_result.where(region_result >= 0, float('nan')) + return region_result + + +def compare(KD_path, KetQuaPhanLoaiDat, CODE_MAP, HT_MAP): + gdf = gpd.read_file(KD_path, crs="EPSG:9209") + polygon = gdf.geometry.values + label = gdf.tenchu.values + ouput_image = rioxarray.open_rasterio(KetQuaPhanLoaiDat) + code_tq = HT_MAP["TQ"]["data"][0] + code_pnn = HT_MAP["PNN"]["data"][0] + result = {} + for key, values in HT_MAP.items(): + print(f"process {key}") + array_list = [] + for i in range(len(polygon)): + po = polygon[i] + lb = label[i] + code_lb = CODE_MAP.get(lb, code_tq) + try: + qr = ouput_image.rio.clip([po], "EPSG:9209") + if code_lb in values["data"]: + if code_lb == code_pnn: + qr = qr.where((qr != float(code_pnn)), np.nan) + # qr = qr.where((qr != 3.0), np.nan) + elif code_lb == code_tq: + qr = qr.where((qr != float(code_pnn)), np.nan) + qr = qr.where((qr != 3.0), np.nan) + else: + qr = qr.where(qr != float(code_lb), np.nan) + else: + qr.values[:, :, :] = np.nan + array_list.append(qr) + except Exception as e: + pass + result.update({key: array_list}) + return result + + +def save_result(result, HT_MAP): + # cmap = ListedColormap(colors) + save_path = "ThuanHoa/KetQua" + if not os.path.exists(save_path): + os.mkdir(save_path) + + for k, v in result.items(): + rs = merge_arrays(v, nodata = np.nan) + rs.rio.to_raster(f"{save_path}/{k}.tif") + print(f"save {save_path}/{k}.tif") + # img = rs.plot(cmap=cmap, add_colorbar=False) + # cbar = plt.colorbar(img) + # cbar.ax.set_yticklabels(labels) + # plt.title(f'{HT_MAP[k]["name"]}') + # plt.axis('off') + # plt.show() \ No newline at end of file diff --git a/region/ST_region.dbf b/region/ST_region.dbf new file mode 100644 index 0000000000000000000000000000000000000000..678286543524df218fa955da4f7b6828054ffd46 GIT binary patch literal 735 zcmZRsW@Te!U|?`$oC~5*K{x}0r%QZLYC2fh4@B~DqbdPPX6EM=g9M#HgdB)K0zmx% zMfqR>uzofqIS>;lS(cfXoC;)u^&6qd1NFOPf{kausvjs>Qk0oof=9n|elFNhtonhH zxutm^>#^IPn_83(mW9fn02_rMcoh^Z^b8Qd*V{z_Kd>|j&QDedDN4*s$FC4o*3!ff vq0c$9q!Lvc2hZ5h)X+FQu_#5s5Qhq^!f=~(;VcD^ow}qOh1Gm|2}%I~&^2FV literal 0 HcmV?d00001 diff --git a/region/ST_region.prj b/region/ST_region.prj new file mode 100644 index 0000000..0ee7d78 --- /dev/null +++ b/region/ST_region.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433],AUTHORITY["EPSG",4326]] \ No newline at end of file diff --git a/region/ST_region.shp b/region/ST_region.shp new file mode 100644 index 0000000000000000000000000000000000000000..bed95f17fc887a7b19ee11341e489bb74429371a GIT binary patch literal 82428 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 ALI3~& literal 0 HcmV?d00001 diff --git a/train/ST_training data_updated_1130points.prj b/train/ST_training data_updated_1130points.prj new file mode 100644 index 0000000..0202b8e --- /dev/null +++ b/train/ST_training data_updated_1130points.prj @@ -0,0 +1 @@ +PROJCS["WGS_1984_UTM_Zone_48N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] \ No newline at end of file diff --git a/train/ST_training data_updated_1130points.qml b/train/ST_training data_updated_1130points.qml new file mode 100644 index 0000000..6bda571 --- /dev/null +++ b/train/ST_training data_updated_1130points.qml @@ -0,0 +1,835 @@ + + + + 1 + 1 + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + "No" + + 0 + diff --git a/train/ST_training data_updated_1130points.qpj b/train/ST_training data_updated_1130points.qpj new file mode 100644 index 0000000..18131bc --- /dev/null +++ b/train/ST_training data_updated_1130points.qpj @@ -0,0 +1 @@ +PROJCS["WGS 84 / UTM zone 48N",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",105],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32648"]] diff --git a/train/ST_training data_updated_1130points.shp b/train/ST_training data_updated_1130points.shp new file mode 100644 index 0000000000000000000000000000000000000000..050307b3529d05634fbba23f6302f248a545e315 GIT binary patch literal 31740 zcmZwQc{r8byEt%rZ$+XaR4UqgOCm!_BGW@fBqhlh6{UnSB}IdwLZ+0Yq|j(+qLhkg zP$88R8W2fjNJ&b++gk5^j_Y@w^T%<$-_N_&Gp%Q>d);e2`xOwFAtLa9{-54pD@Q

7@E7M||H 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 literal 0 HcmV?d00001 diff --git a/train/ST_training data_updated_1130points.shx b/train/ST_training data_updated_1130points.shx new file mode 100644 index 0000000000000000000000000000000000000000..18f235d0fa63e4f38a7ad094aaba2fb796aea8f7 GIT binary patch literal 9140 zcmZwGaVY11p1|>0KkMx8to5_jI_IpPpS9LnKWCkD*2+(lq)D12Nt&!ANs=^8liV~( zlO!ii&PmcVO_DTenj}q=xDX#;66v=p|F_$H*Vm({g!zUc! z6zBMs>)horF9Rb&gy#?u%M@m_h!w15GrQQwAx`imm-wDLJmfiV1HTG2g}c5=VkQe% z&KfqcgO51Kan5p)tK8xN&v+9U8N~#qGmoXb#|E~smrps$Y0h(*8{FdwuL6G@UR-EC zKaOK6b6Ctu*0F`%?B_5i`HFA2#t%H=1-}GF#W0y!EaY9@=L2@~F`w}{U+^{Gaho6c ziJt@iE}DtVU_Q%O%|^EKAqP0d87^>zo80FquLGka8P7E4vV>KvXDfU7gd?2d9N%)C zyFBJ);7>w?&(Tj}nZj%qv4XX1W*7T7#0kFS65n%&hdk$P;Mbw1aM#yK%wz$}S;HoF z@DT?&&RH&Um0LXE8E*prK8guUXC6y=j}2^NFQ0Oh)12orH@L?WUIoTPFpjCrVKFON z#};<8pTnHwE56|xKk$eb{1W)n7$!4|g}lrAe85gV<}*I$3%=$%Zu28Q@pE8oG!vP@ ze3r4Ajcn&b4seV!T;K{fxzAHx2mUOQ@l0bbOIXEvwz7v$IKnB;@h#W6%VS;!#)Syq zv$$BMFq=iJU@e>3#Xb&kf-kwm_uSzj&v_g8O{gi{^-U5pS-^7Eu!$Xf#6ga8mWy2F z77uvFo51)eCNQ0OEag2mu#LTZ%27^pp3B_e9#41``11&c7Z{q;&r_MhVpg(_E$n7L zhdIete8V+<;1MtQB`_g|$;@IQ@A5t$u#=DZjL-RkulbJK{K!xI9Qcc9CNhKhEMqkr z+0KU?;23APz!h$CpQpSIOpIha)0oQ=R!H zvsuIn*0Pyh?Bft8_>xO}&mA7}oVS5Vp{8(GQW7&+z;f2Gi5+~zL5_2li(KUv4|v9# zz<-Eh0@IntQr=?&+t|ye9OX3Uxy%jj@q|}_$q|fWDsx!OO4hN3-R$QuC;5tRxW*4W z;sw71{$mW2nZ-ih<$XS2Cm-_}pYsJ@^BuSOk)QZEFeRFa%wRstSj|SZ^C1T~#u+Yf zg`3>xDX#go7u%a z4sn7nxy1L};UUj?8~CeGQ@HC_Nz7yc%UQ!FcJL7gInG%wa+O;=;2CcM)1sKbbmp;? z_t?NT_VOu5In8-4bAx+4;Z@+b5sYJak)e5go5NyOvW_k6WCHGbd`FZd-e zJ%-85Vj=JHJ|D1?kNJ$x`GT+cj@$gmPy8JCT{IJ!!F-mnnvHDdLk@6^GhE;bH@VMK zUI%7GGM;J7WeKZT&sO&E2}d}^IlkpOcX`aqz+Z<5pPOIDGKJYJVg+m2%r5qEh!cFt zCBEkl4|&epz|2rnxGOV>nJi#AYuLmNKH?z9Im<<^a*GE%<4xdiqL{#R=CPFb*uXaS z@+n6-&3P_!gL^#TRbW;G}VKTE=$h*AH z2khiyKI3z~;A_6)Hb3$cKL=(xO}&mA7}oVS6$ z4>g6mexJll7OnCY-2BT+W+m&`!fy6+n3H_PH(cWf9`S--0`p>+%q$l2F7NXJJNcN; z_?$2Jn(w&HkNm{Xf&U!ML}oCbWvpf++xd_K9ODcZxWY~D^OVxx+)A^ER*`)D-S2 zNMa@nSk4+Yv4f8|$Z^hck*nO|0nd07_%BgRU^??y%6n{J8+-YbqnzeEm$|_`p71KL zFoJPRWe$s3$vU>MoBbT-Bwz6j*Z6@)yx^C>e~n=>vslQxyw3;h>9EKMg bn%|!)S;rQ3v!BD9H2 literal 0 HcmV?d00001 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..57b36fc --- /dev/null +++ b/utils.py @@ -0,0 +1,6 @@ +import geopandas as gpd + + +def load_data_geo(path: str): + gdf = gpd.read_file(path) + return gdf \ No newline at end of file