option as we did above."
749 | ]
750 | },
751 | {
752 | "cell_type": "markdown",
753 | "metadata": {},
754 | "source": [
755 | "Let's save the output of the search into a list and then beautify the display by creating a pandas table."
756 | ]
757 | },
758 | {
759 | "cell_type": "code",
760 | "execution_count": null,
761 | "metadata": {},
762 | "outputs": [],
763 | "source": [
764 | "list_prod = gs.read_command(\"i.sentinel.download\", \n",
765 | " flags=\"l\", \n",
766 | " producttype=\"S2MSI2A\", \n",
767 | " map=\"urban_area_raleigh\",\n",
768 | " settings=os.path.join(homedir, \"esa_credentials.txt\"), \n",
769 | " footprints=\"s2_footprints\", # we save the footprints in a vector file\n",
770 | " start=\"2022-02-01\", \n",
771 | " end=\"2022-05-31\", \n",
772 | " clouds=\"5\",\n",
773 | " sort=\"ingestiondate\",\n",
774 | " limit=10)"
775 | ]
776 | },
777 | {
778 | "cell_type": "code",
779 | "execution_count": null,
780 | "metadata": {},
781 | "outputs": [],
782 | "source": [
783 | "# print plain list\n",
784 | "list_prod"
785 | ]
786 | },
787 | {
788 | "cell_type": "code",
789 | "execution_count": null,
790 | "metadata": {},
791 | "outputs": [],
792 | "source": [
793 | "import pandas as pd\n",
794 | "from io import StringIO\n",
795 | "\n",
796 | "pd.read_csv(StringIO(list_prod), delimiter=\" \", usecols=[0, 1, 2, 4, 5, 6, 7],\n",
797 | " names=['uuid', 'scene', 'date', 'cloud', 'product', 'size', 'unit'])"
798 | ]
799 | },
800 | {
801 | "cell_type": "code",
802 | "execution_count": null,
803 | "metadata": {},
804 | "outputs": [],
805 | "source": [
806 | "# list available vector maps in sentinel2\n",
807 | "gs.list_grouped(type=\"vector\")[\"sentinel2\"]"
808 | ]
809 | },
810 | {
811 | "cell_type": "code",
812 | "execution_count": null,
813 | "metadata": {},
814 | "outputs": [],
815 | "source": [
816 | "# diplay footprints (you may want to zoom out a bit)\n",
817 | "fp_map = gj.InteractiveMap(width = 400, use_region=True, tiles=\"OpenStreetMap\")\n",
818 | "fp_map.add_vector(\"s2_footprints\")\n",
819 | "fp_map.add_vector(\"urban_area_raleigh\")\n",
820 | "fp_map.add_layer_control(position = \"bottomright\")\n",
821 | "fp_map.show()"
822 | ]
823 | },
824 | {
825 | "cell_type": "markdown",
826 | "metadata": {},
827 | "source": [
828 | "The next step is to download the scene or scenes of interest. Just remove the `-l` flag and add the `output` option in order to define the path to the output directory where data should be saved. \n",
829 | "\n",
830 | "As download might take quite some time, we'll **skip this part** and directly use an already prepared set of smaller, ready to import scenes which we downloaded above. Still, we leave an example below for future reference :)\n",
831 | "\n",
832 | "Go to section **\"Importing Sentinel 2 data\"**"
833 | ]
834 | },
835 | {
836 | "cell_type": "code",
837 | "execution_count": null,
838 | "metadata": {},
839 | "outputs": [],
840 | "source": [
841 | "# Example: download of a selected scene (2022-06-17, T15:58:29Z)\n",
842 | "# gs.run_command(\"i.sentinel.download\", \n",
843 | "# settings=s2_credentials, \n",
844 | "# uuid=\"cfa30609-5627-4788-b7ff-768e2df99975\", \n",
845 | "# output=s2_data)"
846 | ]
847 | },
848 | {
849 | "cell_type": "markdown",
850 | "metadata": {},
851 | "source": [
852 | "### Importing Sentinel-2 data\n",
853 | "\n",
854 | "Before importing or linking Sentinel-2 data we print a list of filtered raster files including projection match \n",
855 | "(1 for match, otherwise 0). If the CRS of the input data differs from that of the current location, you should \n",
856 | "consider reprojection (`-r`flag) or creating a new location for import.\n",
857 | "\n",
858 | "*Important*: Data will be imported into the new location by means of the [i.sentinel.import](https://grass.osgeo.org/grass-stable/manuals/addons/i.sentinel.import.html) tool. \n",
859 | "The command will, by default, import **all** Sentinel bands from `input` directory recursively. \n",
860 | "Before importing the data, let’s check content of the input directory by means of the `-p` flag."
861 | ]
862 | },
863 | {
864 | "cell_type": "code",
865 | "execution_count": null,
866 | "metadata": {},
867 | "outputs": [],
868 | "source": [
869 | "# Check list of pre-downloaded Sentinel-2 scenes, with i.sentinel.import (-p: print)\n",
870 | "gs.parse_command(\"i.sentinel.import\", \n",
871 | " flags=\"p\", \n",
872 | " input=s2_data)"
873 | ]
874 | },
875 | {
876 | "cell_type": "markdown",
877 | "metadata": {},
878 | "source": [
879 | "To speed up things, we'll limit the S2 data import to the RGB and NIR bands (2, 3, 4, 8A) in 10 m spatial resolution using the `pattern` option. Let's first print the bands that will be imported:"
880 | ]
881 | },
882 | {
883 | "cell_type": "code",
884 | "execution_count": null,
885 | "metadata": {},
886 | "outputs": [],
887 | "source": [
888 | "# print only to test band selection\n",
889 | "gs.parse_command(\"i.sentinel.import\", \n",
890 | " flags=\"p\", \n",
891 | " input=s2_data, \n",
892 | " pattern=\"B(02|03|04|08)_10m\")"
893 | ]
894 | },
895 | {
896 | "cell_type": "markdown",
897 | "metadata": {},
898 | "source": [
899 | "By default, input data are imported into GRASS and converted into GRASS native data format.\n",
900 | "Alternatively, data can be linked if the `-l` flag is provided. It is also\n",
901 | "useful to import cloud mask vector features (`-c` flag). In addition, we'll use the \n",
902 | "`register_output` option to produce a timestamp plain text file\n",
903 | "which will be used later on to create a time series."
904 | ]
905 | },
906 | {
907 | "cell_type": "code",
908 | "execution_count": null,
909 | "metadata": {},
910 | "outputs": [],
911 | "source": [
912 | "# for S2 import, allow for using 2GB of RAM for faster operations.\n",
913 | "# (s2_data and s2_timestamps are defined above)\n",
914 | "# this takes up to a few minutes...\n",
915 | "gs.parse_command(\"i.sentinel.import\", \n",
916 | " flags=\"rcsj\", \n",
917 | " input=s2_data, \n",
918 | " pattern=\"B(02|03|04|08)_10m\", \n",
919 | " memory=4000, \n",
920 | " extent=\"input\",\n",
921 | " register_output=s2_timestamps)"
922 | ]
923 | },
924 | {
925 | "cell_type": "code",
926 | "execution_count": null,
927 | "metadata": {},
928 | "outputs": [],
929 | "source": [
930 | "# list selected raster maps\n",
931 | "gs.list_grouped(type=\"raster\")['sentinel2']"
932 | ]
933 | },
934 | {
935 | "cell_type": "code",
936 | "execution_count": null,
937 | "metadata": {},
938 | "outputs": [],
939 | "source": [
940 | "# check metadata of one of the imported bands\n",
941 | "gs.raster_info(map=\"T17SQV_20220617T155829_B03_10m\")[\"comments\"]"
942 | ]
943 | },
944 | {
945 | "cell_type": "code",
946 | "execution_count": null,
947 | "metadata": {},
948 | "outputs": [],
949 | "source": [
950 | "# print timestamp file for inspection\n",
951 | "with open(s2_timestamps, 'r') as f:\n",
952 | " content = f.read()\n",
953 | " print(content)\n",
954 | " f.close()"
955 | ]
956 | },
957 | {
958 | "cell_type": "markdown",
959 | "metadata": {},
960 | "source": [
961 | "
\n",
962 | "
Semantic labels\n",
963 | "A fairly new concept within GRASS GIS is semantic labels. These are especially relevant for satellite imagery as they allow us to identify to which sensor and band a given raster corresponds to. These labels are particularly relevant when working with satellite image collections and also when classifying different scenes. We will see it later, but by generating a spectral signature for a given set of bands, it can be re-used to classify another scene as long as the semantic labels are the same. Be ware – although it is possible to re-use spectral signatures to any scene with the same bands, temporal changes (seasons, weather impact) limit their applicability only to scenes obtained more or less at the same time."
964 | ]
965 | },
966 | {
967 | "cell_type": "markdown",
968 | "metadata": {},
969 | "source": [
970 | "### Displaying maps with `grass.jupyter` functions"
971 | ]
972 | },
973 | {
974 | "cell_type": "code",
975 | "execution_count": null,
976 | "metadata": {},
977 | "outputs": [],
978 | "source": [
979 | "# create Map instance\n",
980 | "b3_map = gj.Map(width=400)\n",
981 | "# add a raster, vector and legend to the map\n",
982 | "b3_map.d_rast(map=\"T17SQV_20220617T155829_B03_10m\")\n",
983 | "b3_map.d_vect(map=\"lakes\")\n",
984 | "b3_map.d_legend(raster=\"T17SQV_20220617T155829_B03_10m\", \n",
985 | " title=\"Reflectance\", \n",
986 | " fontsize=10, at=(70, 93, 80, 90), flags=\"b\")\n",
987 | "b3_map.d_barscale()\n",
988 | "# display map\n",
989 | "b3_map.show()"
990 | ]
991 | },
992 | {
993 | "cell_type": "code",
994 | "execution_count": null,
995 | "metadata": {},
996 | "outputs": [],
997 | "source": [
998 | "# set color table of bands 4, 3 and 2 to grey\n",
999 | "gs.run_command(\"r.colors\", \n",
1000 | " map=\"T17SQV_20220617T155829_B04_10m,T17SQV_20220617T155829_B03_10m,T17SQV_20220617T155829_B02_10m\", \n",
1001 | " color=\"grey\")"
1002 | ]
1003 | },
1004 | {
1005 | "cell_type": "code",
1006 | "execution_count": null,
1007 | "metadata": {},
1008 | "outputs": [],
1009 | "source": [
1010 | "# color enhancing for RGB composition\n",
1011 | "gs.run_command(\"i.colors.enhance\", \n",
1012 | " red=\"T17SQV_20220617T155829_B04_10m\",\n",
1013 | " green=\"T17SQV_20220617T155829_B03_10m\", \n",
1014 | " blue=\"T17SQV_20220617T155829_B02_10m\",\n",
1015 | " strength=92)"
1016 | ]
1017 | },
1018 | {
1019 | "cell_type": "code",
1020 | "execution_count": null,
1021 | "metadata": {},
1022 | "outputs": [],
1023 | "source": [
1024 | "# set region to \"elevation\" map and align to the S2 data\n",
1025 | "gs.run_command(\"g.region\", \n",
1026 | " raster=\"elevation\",\n",
1027 | " align=\"T17SQV_20220617T155829_B04_10m\",\n",
1028 | " flags=\"p\")"
1029 | ]
1030 | },
1031 | {
1032 | "cell_type": "code",
1033 | "execution_count": null,
1034 | "metadata": {},
1035 | "outputs": [],
1036 | "source": [
1037 | "# display the enhanced RGB combination\n",
1038 | "rgb = gj.Map(width=400, use_region=True)\n",
1039 | "rgb.d_rgb(red=\"T17SQV_20220617T155829_B04_10m\",\n",
1040 | " green=\"T17SQV_20220617T155829_B03_10m\", \n",
1041 | " blue=\"T17SQV_20220617T155829_B02_10m\")\n",
1042 | "rgb.show()"
1043 | ]
1044 | },
1045 | {
1046 | "cell_type": "markdown",
1047 | "metadata": {},
1048 | "source": [
1049 | "## 8. Spectral indices of vegetation and water"
1050 | ]
1051 | },
1052 | {
1053 | "cell_type": "markdown",
1054 | "metadata": {},
1055 | "source": [
1056 | "We will use i.vi and i.wi (addon) to estimate NDVI and NDWI vegetation and water indices. See [i.vi](https://grass.osgeo.org/grass-stable/manuals/i.vi.html) and [i.wi](https://grass.osgeo.org/grass-stable/manuals/addons/i.wi.html) for more other available indices."
1057 | ]
1058 | },
1059 | {
1060 | "cell_type": "code",
1061 | "execution_count": null,
1062 | "metadata": {},
1063 | "outputs": [],
1064 | "source": [
1065 | "# estimate vegetation indices\n",
1066 | "gs.run_command(\"i.vi\", \n",
1067 | " red=\"T17SQV_20220528T155819_B04_10m\", \n",
1068 | " nir=\"T17SQV_20220528T155819_B08_10m\", \n",
1069 | " output=\"T17SQV_20220528T155819_NDVI_10m\", \n",
1070 | " viname=\"ndvi\")\n",
1071 | "\n",
1072 | "# add semantic label\n",
1073 | "gs.run_command(\"r.support\", \n",
1074 | " map=\"T17SQV_20220528T155819_NDVI_10m\", \n",
1075 | " semantic_label=\"S2_NDVI\")"
1076 | ]
1077 | },
1078 | {
1079 | "cell_type": "code",
1080 | "execution_count": null,
1081 | "metadata": {},
1082 | "outputs": [],
1083 | "source": [
1084 | "# install extension\n",
1085 | "gs.run_command(\"g.extension\", extension=\"i.wi\")"
1086 | ]
1087 | },
1088 | {
1089 | "cell_type": "code",
1090 | "execution_count": null,
1091 | "metadata": {},
1092 | "outputs": [],
1093 | "source": [
1094 | "# estimate water indices\n",
1095 | "gs.run_command(\"i.wi\", \n",
1096 | " green=\"T17SQV_20220528T155819_B03_10m\", \n",
1097 | " nir=\"T17SQV_20220528T155819_B08_10m\", \n",
1098 | " output=\"T17SQV_20220528T155819_NDWI_10m\", \n",
1099 | " winame=\"ndwi_mf\")\n",
1100 | "\n",
1101 | "# set ndwi color palette\n",
1102 | "gs.run_command(\"r.colors\", map=\"T17SQV_20220528T155819_NDWI_10m\", color=\"ndwi\")\n",
1103 | "\n",
1104 | "# add semantic label\n",
1105 | "gs.run_command(\"r.support\", \n",
1106 | " map=\"T17SQV_20220528T155819_NDWI_10m\", \n",
1107 | " semantic_label=\"S2_NDWI\")"
1108 | ]
1109 | },
1110 | {
1111 | "cell_type": "code",
1112 | "execution_count": null,
1113 | "metadata": {},
1114 | "outputs": [],
1115 | "source": [
1116 | "# check metadata of NDVI\n",
1117 | "gs.raster_info(map=\"T17SQV_20220528T155819_NDVI_10m\")"
1118 | ]
1119 | },
1120 | {
1121 | "cell_type": "code",
1122 | "execution_count": null,
1123 | "metadata": {},
1124 | "outputs": [],
1125 | "source": [
1126 | "# interactive maps\n",
1127 | "idx_map = gj.InteractiveMap(width = 400, use_region=True, tiles=\"OpenStreetMap\")\n",
1128 | "idx_map.add_raster(\"T17SQV_20220528T155819_NDVI_10m\", opacity=0.7)\n",
1129 | "idx_map.add_raster(\"T17SQV_20220528T155819_NDWI_10m\", opacity=0.7)\n",
1130 | "idx_map.add_layer_control(position = \"bottomright\")\n",
1131 | "idx_map.show()\n",
1132 | "# ... use the layer selector in the corner to enable/disable the NDVI/NDWI layers"
1133 | ]
1134 | },
1135 | {
1136 | "cell_type": "markdown",
1137 | "metadata": {},
1138 | "source": [
1139 | "#### GRASS GIS maps as numpy arrays\n",
1140 | "\n",
1141 | "GRASS maps can be read as numpy arrays thanks to the array function of the grass.script library. This facilitates many operations with python libraries that require an array as input. In this case, we demonstrate its use plotting an histogram."
1142 | ]
1143 | },
1144 | {
1145 | "cell_type": "code",
1146 | "execution_count": null,
1147 | "metadata": {},
1148 | "outputs": [],
1149 | "source": [
1150 | "# Import required libraries\n",
1151 | "import numpy as np\n",
1152 | "import seaborn as sns\n",
1153 | "import matplotlib.pyplot as plt\n",
1154 | "from grass.script import array as garray\n",
1155 | "\n",
1156 | "# Read NDVI as numpy array\n",
1157 | "ndvi = garray.array(mapname=\"T17SQV_20220528T155819_NDVI_10m\", null=\"nan\")\n",
1158 | "ndwi = garray.array(mapname=\"T17SQV_20220528T155819_NDWI_10m\", null=\"nan\")\n",
1159 | "print(ndvi.shape,ndwi.shape)"
1160 | ]
1161 | },
1162 | {
1163 | "cell_type": "code",
1164 | "execution_count": null,
1165 | "metadata": {
1166 | "scrolled": true
1167 | },
1168 | "outputs": [],
1169 | "source": [
1170 | "# Plot NDVI and NDWI\n",
1171 | "sns.set_style('darkgrid')\n",
1172 | "fig, axs = plt.subplots(1, 2, figsize=(7, 7))\n",
1173 | "sns.histplot(ax=axs[0], data=ndvi.ravel(), kde=True, color=\"olive\")\n",
1174 | "sns.histplot(ax=axs[1], data=ndwi.ravel(), kde=True, color=\"skyblue\")\n",
1175 | "plt.show()"
1176 | ]
1177 | },
1178 | {
1179 | "cell_type": "markdown",
1180 | "metadata": {},
1181 | "source": [
1182 | "## 10. NDVI time series data processing"
1183 | ]
1184 | },
1185 | {
1186 | "cell_type": "markdown",
1187 | "metadata": {},
1188 | "source": [
1189 | "### A few concepts of time series data processing in GRASS GIS\n",
1190 | "\n",
1191 | "GRASS GIS offers specialized tools for spatio-temporal data\n",
1192 | "processing, see GRASS documentation [temporalintro](https://grass.osgeo.org/grass-stable/manuals/temporalintro.html) for details and the [temporal data processing](https://grasswiki.osgeo.org/wiki/Temporal_data_processing) wiki for examples and a workflow tutorial.\n",
1193 | "\n",
1194 | "GRASS introduces three special data types that are designed to handle time-series:\n",
1195 | "\n",
1196 | "* *Space-time raster datasets* (`strds`) for managing raster map time series.\n",
1197 | "\n",
1198 | "* *Space-time 3D raster datasets* (`str3ds`) for managing 3D raster map time series.\n",
1199 | "\n",
1200 | "* *Space-time vector datasets* (`stvds`) for managing vector map time series.\n",
1201 | " \n",
1202 | "
\n",
1203 | "
\n",
1207 | "\n"
1208 | ]
1209 | },
1210 | {
1211 | "cell_type": "markdown",
1212 | "metadata": {},
1213 | "source": [
1214 | "### Create space-time dataset\n",
1215 | "\n",
1216 | "At this moment a new space-time dataset can be created by means of [t.create](https://grass.osgeo.org/grass-stable/manuals/t.create.html) and all imported Sentinel bands registered with [t.register](https://grass.osgeo.org/grass-stable/manuals/t.register.html) and the timestamps file we created when we imported S2 bands."
1217 | ]
1218 | },
1219 | {
1220 | "cell_type": "code",
1221 | "execution_count": null,
1222 | "metadata": {},
1223 | "outputs": [],
1224 | "source": [
1225 | "gs.run_command(\"t.create\", \n",
1226 | " output=\"s2_nc\", \n",
1227 | " title=\"Sentinel L2A - North Carolina\", \n",
1228 | " desc=\"Tile T17SQV - 2022\")\n",
1229 | "\n",
1230 | "gs.run_command(\"t.register\", \n",
1231 | " input=\"s2_nc\", \n",
1232 | " file=s2_timestamps)"
1233 | ]
1234 | },
1235 | {
1236 | "cell_type": "markdown",
1237 | "metadata": {},
1238 | "source": [
1239 | "Let’s check basic metadata with [t.info](https://grass.osgeo.org/grass-stable/manuals/t.info.html) and list the registered maps with [t.rast.list](https://grass.osgeo.org/grass-stable/manuals/t.rast.list.html)."
1240 | ]
1241 | },
1242 | {
1243 | "cell_type": "code",
1244 | "execution_count": null,
1245 | "metadata": {},
1246 | "outputs": [],
1247 | "source": [
1248 | "# Print time series info\n",
1249 | "print(gs.read_command(\"t.info\", input=\"s2_nc\"))"
1250 | ]
1251 | },
1252 | {
1253 | "cell_type": "code",
1254 | "execution_count": null,
1255 | "metadata": {},
1256 | "outputs": [],
1257 | "source": [
1258 | "# List registered bands in the space-time cube\n",
1259 | "print(gs.read_command(\"t.rast.list\", \n",
1260 | " input=\"s2_nc\", \n",
1261 | " columns=\"name,start_time,semantic_label\"))"
1262 | ]
1263 | },
1264 | {
1265 | "cell_type": "markdown",
1266 | "metadata": {},
1267 | "source": [
1268 | "We'll now use a special syntaxis to list only band 4 raster maps withing the time series:"
1269 | ]
1270 | },
1271 | {
1272 | "cell_type": "code",
1273 | "execution_count": null,
1274 | "metadata": {},
1275 | "outputs": [],
1276 | "source": [
1277 | "# List only band 4 maps\n",
1278 | "print(gs.read_command(\"t.rast.list\", \n",
1279 | " input=\"s2_nc.S2_4\", \n",
1280 | " columns=\"name,start_time,semantic_label\"))"
1281 | ]
1282 | },
1283 | {
1284 | "cell_type": "markdown",
1285 | "metadata": {},
1286 | "source": [
1287 | "### NDVI Space-Time computation\n",
1288 | "\n",
1289 | "For NDVI computation the 4th and 8th bands are required, as we saw above for a single map. \n",
1290 | "Now, we will create a time series of NDVI maps. We will take advantage of the semantic labels syntax and use\n",
1291 | "[t.rast.mapcalc](https://grass.osgeo.org/grass-stable/manuals/t.rast.mapcalc.html) to estimate NDVI for all the timestamps in the time series, using band 4 and 8 subsets."
1292 | ]
1293 | },
1294 | {
1295 | "cell_type": "code",
1296 | "execution_count": null,
1297 | "metadata": {},
1298 | "outputs": [],
1299 | "source": [
1300 | "gs.run_command(\"t.rast.mapcalc\", \n",
1301 | " inputs=\"s2_nc.S2_8,s2_nc.S2_4\", \n",
1302 | " output=\"s2_ndvi\", \n",
1303 | " basename=\"s2_ndvi\",\n",
1304 | " expression=\"float(s2_nc.S2_8 - s2_nc.S2_4) / (s2_nc.S2_8 + s2_nc.S2_4)\")"
1305 | ]
1306 | },
1307 | {
1308 | "cell_type": "markdown",
1309 | "metadata": {},
1310 | "source": [
1311 | "When computation is finished, the *ndvi* color table can be set with [t.rast.colors](https://grass.osgeo.org/grass-stable/manuals/t.rast.colors.html):"
1312 | ]
1313 | },
1314 | {
1315 | "cell_type": "code",
1316 | "execution_count": null,
1317 | "metadata": {},
1318 | "outputs": [],
1319 | "source": [
1320 | "gs.run_command(\"t.rast.colors\", input=\"s2_ndvi\", color=\"ndvi\")"
1321 | ]
1322 | },
1323 | {
1324 | "cell_type": "code",
1325 | "execution_count": null,
1326 | "metadata": {},
1327 | "outputs": [],
1328 | "source": [
1329 | "print(gs.read_command(\"t.info\", input=\"s2_ndvi\"))"
1330 | ]
1331 | },
1332 | {
1333 | "cell_type": "markdown",
1334 | "metadata": {},
1335 | "source": [
1336 | "### Time series plots"
1337 | ]
1338 | },
1339 | {
1340 | "cell_type": "markdown",
1341 | "metadata": {},
1342 | "source": [
1343 | "Let’s check content of the new dataset by means of [t.rast.list](https://grass.osgeo.org/grass-stable/manuals/t.rast.list.html):"
1344 | ]
1345 | },
1346 | {
1347 | "cell_type": "code",
1348 | "execution_count": null,
1349 | "metadata": {},
1350 | "outputs": [],
1351 | "source": [
1352 | "print(gs.read_command(\"t.rast.list\", \n",
1353 | " input=\"s2_ndvi\", \n",
1354 | " columns=\"name,start_time,min,max\"))"
1355 | ]
1356 | },
1357 | {
1358 | "cell_type": "markdown",
1359 | "metadata": {},
1360 | "source": [
1361 | "If we save the previous output to a file, we can then plot the min and max time series:"
1362 | ]
1363 | },
1364 | {
1365 | "cell_type": "code",
1366 | "execution_count": null,
1367 | "metadata": {},
1368 | "outputs": [],
1369 | "source": [
1370 | "gs.run_command(\"t.rast.list\", \n",
1371 | " input=\"s2_ndvi\", \n",
1372 | " columns=\"name,start_time,min,max\",\n",
1373 | " format=\"csv\",\n",
1374 | " separator=\"comma\",\n",
1375 | " output=os.path.join(homedir,\"ndvi.csv\"))"
1376 | ]
1377 | },
1378 | {
1379 | "cell_type": "code",
1380 | "execution_count": null,
1381 | "metadata": {},
1382 | "outputs": [],
1383 | "source": [
1384 | "# Read the csv and plot\n",
1385 | "ndvi = pd.read_csv(os.path.join(homedir,\"ndvi.csv\"))\n",
1386 | "ndvi.plot(0, [2,3], subplots=False)"
1387 | ]
1388 | },
1389 | {
1390 | "cell_type": "markdown",
1391 | "metadata": {},
1392 | "source": [
1393 | "We could also use [t.rast.univar](https://grass.osgeo.org/grass-stable/manuals/t.rast.univar.html) to obtain extended statistics:"
1394 | ]
1395 | },
1396 | {
1397 | "cell_type": "code",
1398 | "execution_count": null,
1399 | "metadata": {},
1400 | "outputs": [],
1401 | "source": [
1402 | "# Get extended univar stats and save them as a csv file\n",
1403 | "gs.run_command(\"t.rast.univar\",\n",
1404 | " flags=\"e\",\n",
1405 | " input=\"s2_ndvi\",\n",
1406 | " output=os.path.join(homedir,\"ndvi_ext_stats.csv\"),\n",
1407 | " separator=\"comma\")"
1408 | ]
1409 | },
1410 | {
1411 | "cell_type": "code",
1412 | "execution_count": null,
1413 | "metadata": {},
1414 | "outputs": [],
1415 | "source": [
1416 | "# Read the csv and plot\n",
1417 | "ndvi = pd.read_csv(os.path.join(homedir,\"ndvi_ext_stats.csv\"))\n",
1418 | "ndvi['start'] = pd.to_datetime(ndvi.start, format=\"%Y-%m-%d\", exact=False)\n",
1419 | "ndvi.plot.line(1, [3,4,5], subplots=False)"
1420 | ]
1421 | },
1422 | {
1423 | "cell_type": "markdown",
1424 | "metadata": {},
1425 | "source": [
1426 | "### Query time series in a single point\n",
1427 | "\n",
1428 | "`g.region` command allows us to get the coordinates of the center of the computational region, we'll use those to query the NDVI time series."
1429 | ]
1430 | },
1431 | {
1432 | "cell_type": "code",
1433 | "execution_count": null,
1434 | "metadata": {},
1435 | "outputs": [],
1436 | "source": [
1437 | "# Get region center coordinates for query (center_easting, center_northing values)\n",
1438 | "gs.region(complete=True)"
1439 | ]
1440 | },
1441 | {
1442 | "cell_type": "code",
1443 | "execution_count": null,
1444 | "metadata": {},
1445 | "outputs": [],
1446 | "source": [
1447 | "# Query map at center coordinates\n",
1448 | "print(gs.read_command(\"t.rast.what\", \n",
1449 | " strds=\"s2_ndvi\", \n",
1450 | " coordinates=\"637500,221750\", \n",
1451 | " layout=\"col\", \n",
1452 | " flags=\"n\"))"
1453 | ]
1454 | },
1455 | {
1456 | "cell_type": "markdown",
1457 | "metadata": {},
1458 | "source": [
1459 | "### Time series animation\n",
1460 | "\n",
1461 | "Note: [TimeSeriesMap()](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.jupyter.html?highlight=timeseriesmap#module-grass.jupyter.timeseriesmap) of `grass.jupyter` is still experimental and under development."
1462 | ]
1463 | },
1464 | {
1465 | "cell_type": "code",
1466 | "execution_count": null,
1467 | "metadata": {},
1468 | "outputs": [],
1469 | "source": [
1470 | "### YET TO BE SKIPPED - in GRASS GIS 8.2.0 it takes \"forever\", bugfix pending.\n",
1471 | "\n",
1472 | "## reduce resolution for faster display of time series, save original first for later\n",
1473 | "#gs.parse_command(\"g.region\", save=\"default_res\")\n",
1474 | "#gs.parse_command(\"g.region\", flags=\"pa\", res=50)\n",
1475 | " \n",
1476 | "## Display newly created NDVI time series map\n",
1477 | "#ndviseries = gj.TimeSeriesMap(use_region=True)\n",
1478 | "#ndviseries.add_raster_series(\"s2_ndvi\", fill_gaps=False)\n",
1479 | "#ndviseries.d_legend(color=\"black\", at=(10,40,2,6))\n",
1480 | "#ndviseries.d_barscale()\n",
1481 | "#ndviseries.show() # Create TimeSlider\n",
1482 | "\n",
1483 | "# optionally, write out to animated GIF\n",
1484 | "# ndviseries.save(\"image.gif\")"
1485 | ]
1486 | },
1487 | {
1488 | "cell_type": "code",
1489 | "execution_count": null,
1490 | "metadata": {},
1491 | "outputs": [],
1492 | "source": [
1493 | "## restore original region\n",
1494 | "#gs.parse_command(\"g.region\", region=\"default_res\")"
1495 | ]
1496 | },
1497 | {
1498 | "cell_type": "markdown",
1499 | "metadata": {},
1500 | "source": [
1501 | "## 11. Creating an image stack (imagery group)\n",
1502 | "\n",
1503 | "**Stack of maps = imagery group**\n",
1504 | "\n",
1505 | "When you work with a stack of raster maps (e.g., R-G-B channels or more) in GRASS GIS, you can best handle this stack by creating a raster group with [i.group](https://grass.osgeo.org/grass-stable/manuals/i.group.html). It is just based on metadata, so it does not take up more disk space."
1506 | ]
1507 | },
1508 | {
1509 | "cell_type": "code",
1510 | "execution_count": null,
1511 | "metadata": {},
1512 | "outputs": [],
1513 | "source": [
1514 | "# Since imagery groups can not be overwritten, \n",
1515 | "# we delete a potentially leftover \"s2\" group from a previous run\n",
1516 | "gs.run_command(\"g.remove\", \n",
1517 | " type=\"group\", \n",
1518 | " name=\"s2\", \n",
1519 | " flags=\"f\")"
1520 | ]
1521 | },
1522 | {
1523 | "cell_type": "code",
1524 | "execution_count": null,
1525 | "metadata": {},
1526 | "outputs": [],
1527 | "source": [
1528 | "# Generate list of selected S2 maps\n",
1529 | "s2_maps = gs.list_grouped(type=\"raster\", pattern=\"*20220528T155819*\")['sentinel2']\n",
1530 | "print(s2_maps)"
1531 | ]
1532 | },
1533 | {
1534 | "cell_type": "code",
1535 | "execution_count": null,
1536 | "metadata": {},
1537 | "outputs": [],
1538 | "source": [
1539 | "# Create group and subgroup with S2 bands\n",
1540 | "gs.run_command(\"i.group\", group=\"s2\", subgroup=\"s2\", input=s2_maps)\n",
1541 | "print(gs.read_command(\"i.group\", group=\"s2\", flags=\"l\"))"
1542 | ]
1543 | },
1544 | {
1545 | "cell_type": "markdown",
1546 | "metadata": {},
1547 | "source": [
1548 | "## 12. Object recognition with image segmentation"
1549 | ]
1550 | },
1551 | {
1552 | "cell_type": "markdown",
1553 | "metadata": {},
1554 | "source": [
1555 | "We'll use [i.segment](https://grass.osgeo.org/grass-stable/manuals/i.segment.html) to perform image segmentation. The resulting map will be used together with S2 bands, NDVI and NDWI to perform supervised classification."
1556 | ]
1557 | },
1558 | {
1559 | "cell_type": "code",
1560 | "execution_count": null,
1561 | "metadata": {},
1562 | "outputs": [],
1563 | "source": [
1564 | "# Threshold = 0 merges only identical segments; threshold = 1 merges all\n",
1565 | "gs.run_command(\"i.segment\", \n",
1566 | " group=\"s2\", \n",
1567 | " threshold=\"0.05\", \n",
1568 | " minsize=\"100\", \n",
1569 | " output=\"sentinel_segments_min100\", \n",
1570 | " goodness=\"sentinel_segments_goodness_min100\",\n",
1571 | " memory=2000)"
1572 | ]
1573 | },
1574 | {
1575 | "cell_type": "code",
1576 | "execution_count": null,
1577 | "metadata": {},
1578 | "outputs": [],
1579 | "source": [
1580 | "# Display newly created segments raster map\n",
1581 | "segments = gj.InteractiveMap(width = 400, use_region=True)\n",
1582 | "segments.add_raster(\"sentinel_segments_min100\", opacity=0.3)\n",
1583 | "segments.add_raster(\"s2_ndvi_4\", opacity=0.3)\n",
1584 | "segments.add_layer_control(position = \"bottomright\")\n",
1585 | "segments.show()"
1586 | ]
1587 | },
1588 | {
1589 | "cell_type": "code",
1590 | "execution_count": null,
1591 | "metadata": {},
1592 | "outputs": [],
1593 | "source": [
1594 | "# Show univariate statistics of goodness-of-fit raster map, with extended statistics (quartiles)\n",
1595 | "print(gs.read_command(\"r.univar\",\n",
1596 | " map=\"sentinel_segments_goodness_min100\", \n",
1597 | " flags=\"ge\"))"
1598 | ]
1599 | },
1600 | {
1601 | "cell_type": "code",
1602 | "execution_count": null,
1603 | "metadata": {},
1604 | "outputs": [],
1605 | "source": [
1606 | "# Assign color table (low fit values: blue; high fit values: green)\n",
1607 | "gs.run_command(\"r.colors\", \n",
1608 | " map=\"sentinel_segments_goodness_min100\", \n",
1609 | " color=\"byg\", \n",
1610 | " flags=\"e\")"
1611 | ]
1612 | },
1613 | {
1614 | "cell_type": "code",
1615 | "execution_count": null,
1616 | "metadata": {},
1617 | "outputs": [],
1618 | "source": [
1619 | "# Display newly created goodness-of-fit raster map\n",
1620 | "segments = gj.InteractiveMap(width = 400, use_region=True)\n",
1621 | "segments.add_raster(\"sentinel_segments_goodness_min100\", opacity=0.8)\n",
1622 | "segments.add_vector(\"urban_area_raleigh\")\n",
1623 | "segments.add_layer_control(position = \"bottomright\")\n",
1624 | "segments.show()"
1625 | ]
1626 | },
1627 | {
1628 | "cell_type": "markdown",
1629 | "metadata": {},
1630 | "source": [
1631 | "## 13. Supervised Classification: RandomForest\n",
1632 | "\n",
1633 | "We will now demonstrate a very much simplified workflow to perform a supervised [Random Forest classification](https://en.wikipedia.org/wiki/Random_forest).\n",
1634 | "\n",
1635 | "We will feed the following data into the model:\n",
1636 | "\n",
1637 | "- NDVI and NDWI maps (created above)\n",
1638 | "- image segmentation (created above)\n",
1639 | "- random training points extracted from landuse map\n",
1640 | "\n",
1641 | "First we inspect the raster maps available in the current mapset (i.e., `sentinel2`), just to recall their names."
1642 | ]
1643 | },
1644 | {
1645 | "cell_type": "code",
1646 | "execution_count": null,
1647 | "metadata": {},
1648 | "outputs": [],
1649 | "source": [
1650 | "gs.list_grouped(type=\"raster\")[\"sentinel2\"]"
1651 | ]
1652 | },
1653 | {
1654 | "cell_type": "markdown",
1655 | "metadata": {},
1656 | "source": [
1657 | "### Creation of a classification training map by sampling from existing data\n",
1658 | "\n",
1659 | "In order to generate training data for the Sentinel-2 image classification, we will use the [National Land Cover Database (NLCD) 2019](https://www.lib.ncsu.edu/gis/lulc). It is available for download (30m raster map) from [here](https://drive.google.com/open?id=18D99kuotQp_BkxBnkn8OS3qgCeLVwovb&authuser=0). However, we have already prepared the dataset (the `nc_nlcd2019` landuse map). We will use it to perform stratified sampling to retrieve training data."
1660 | ]
1661 | },
1662 | {
1663 | "cell_type": "code",
1664 | "execution_count": null,
1665 | "metadata": {},
1666 | "outputs": [],
1667 | "source": [
1668 | "# Check raster categories of landuse map\n",
1669 | "print(gs.read_command(\"r.category\", \n",
1670 | " map=\"nc_nlcd2019\", \n",
1671 | " separator=\"comma\"))"
1672 | ]
1673 | },
1674 | {
1675 | "cell_type": "code",
1676 | "execution_count": null,
1677 | "metadata": {},
1678 | "outputs": [],
1679 | "source": [
1680 | "# display nc_nlcd2019 landuse raster map\n",
1681 | "lulc = gj.InteractiveMap(width = 400, use_region=True, tiles=\"OpenStreetMap\")\n",
1682 | "lulc.add_raster(\"nc_nlcd2019\", opacity=0.6)\n",
1683 | "lulc.add_layer_control(position = \"bottomright\")\n",
1684 | "lulc.show()"
1685 | ]
1686 | },
1687 | {
1688 | "cell_type": "markdown",
1689 | "metadata": {},
1690 | "source": [
1691 | "We already note differences between the underlying OpenStreetMap data and the 30m NLCD map."
1692 | ]
1693 | },
1694 | {
1695 | "cell_type": "code",
1696 | "execution_count": null,
1697 | "metadata": {},
1698 | "outputs": [],
1699 | "source": [
1700 | "# show simple legend\n",
1701 | "legend = gj.Map(width=400, use_region=True)\n",
1702 | "# at=bottom,top,left,right, percentage of screen coordinates (0,0 is lower left)\n",
1703 | "legend.d_legend(raster=\"nc_nlcd2019\", \n",
1704 | " title=\"Classes\",\n",
1705 | " fontsize=10, at=(10, 90, 50, 90), \n",
1706 | " flags=\"n\")\n",
1707 | "legend.show()"
1708 | ]
1709 | },
1710 | {
1711 | "cell_type": "markdown",
1712 | "metadata": {},
1713 | "source": [
1714 | "### Random sampling from rasterized simplified landuse map\n",
1715 | "\n",
1716 | "We now perform stratified sampling, i.e. we extract for each land use class `n` sampling points, using the GRASS GIS addon [r.sample.category](https://grass.osgeo.org/grass-stable/manuals/addons/r.sample.category.html).\n",
1717 | "\n",
1718 | "First, we install this addon."
1719 | ]
1720 | },
1721 | {
1722 | "cell_type": "code",
1723 | "execution_count": null,
1724 | "metadata": {},
1725 | "outputs": [],
1726 | "source": [
1727 | "gs.run_command(\"g.extension\", extension=\"r.sample.category\")"
1728 | ]
1729 | },
1730 | {
1731 | "cell_type": "code",
1732 | "execution_count": null,
1733 | "metadata": {},
1734 | "outputs": [],
1735 | "source": [
1736 | "# Stratified random sampling, generated vector points\n",
1737 | "gs.run_command(\"r.sample.category\", \n",
1738 | " input=\"nc_nlcd2019\", \n",
1739 | " output=\"landuse_train\", \n",
1740 | " n=\"100\")"
1741 | ]
1742 | },
1743 | {
1744 | "cell_type": "code",
1745 | "execution_count": null,
1746 | "metadata": {},
1747 | "outputs": [],
1748 | "source": [
1749 | "# display newly created vector points map\n",
1750 | "train = gj.InteractiveMap(width = 400, use_region=True)\n",
1751 | "train.add_raster(\"nc_nlcd2019\", opacity=0.7)\n",
1752 | "train.add_vector(\"landuse_train\")\n",
1753 | "train.add_layer_control(position = \"bottomright\")\n",
1754 | "train.show()"
1755 | ]
1756 | },
1757 | {
1758 | "cell_type": "code",
1759 | "execution_count": null,
1760 | "metadata": {},
1761 | "outputs": [],
1762 | "source": [
1763 | "# List column names of vector points map\n",
1764 | "gs.vector_columns(\"landuse_train\", \n",
1765 | " getDict=False)"
1766 | ]
1767 | },
1768 | {
1769 | "cell_type": "code",
1770 | "execution_count": null,
1771 | "metadata": {},
1772 | "outputs": [],
1773 | "source": [
1774 | "# Show vector attribute table\n",
1775 | "gs.vector_db_select(\"landuse_train\")"
1776 | ]
1777 | },
1778 | {
1779 | "cell_type": "code",
1780 | "execution_count": null,
1781 | "metadata": {},
1782 | "outputs": [],
1783 | "source": [
1784 | "# Check column data types\n",
1785 | "print(gs.read_command(\"v.info\", map=\"landuse_train\", flags=\"c\"))"
1786 | ]
1787 | },
1788 | {
1789 | "cell_type": "markdown",
1790 | "metadata": {},
1791 | "source": [
1792 | "Since the machine learning classifier expects raster points as input, we convert the vector sampling points accordingly using [v.to.rast](https://grass.osgeo.org/grass-stable/manuals/v.to.rast.html)."
1793 | ]
1794 | },
1795 | {
1796 | "cell_type": "code",
1797 | "execution_count": null,
1798 | "metadata": {},
1799 | "outputs": [],
1800 | "source": [
1801 | "# Convert points from vector to raster model\n",
1802 | "gs.run_command(\"v.to.rast\", \n",
1803 | " input=\"landuse_train\", \n",
1804 | " output=\"landuse_train\", \n",
1805 | " use=\"attr\", \n",
1806 | " attribute_column=\"nc_nlcd2019\", \n",
1807 | " label_column=\"label\")"
1808 | ]
1809 | },
1810 | {
1811 | "cell_type": "code",
1812 | "execution_count": null,
1813 | "metadata": {},
1814 | "outputs": [],
1815 | "source": [
1816 | "# Check raster categories of new raster training map\n",
1817 | "# Skip reporting on empty cells\n",
1818 | "print(gs.read_command(\"r.report\", \n",
1819 | " map=\"landuse_train\",\n",
1820 | " flags=\"n\"))"
1821 | ]
1822 | },
1823 | {
1824 | "cell_type": "code",
1825 | "execution_count": null,
1826 | "metadata": {},
1827 | "outputs": [],
1828 | "source": [
1829 | "# Display newly created raster map - zoom in to better spot the raster sampling points\n",
1830 | "train = gj.InteractiveMap(width = 400, use_region=True)\n",
1831 | "train.add_raster(\"landuse_train\", opacity=0.8)\n",
1832 | "train.add_layer_control(position = \"bottomright\")\n",
1833 | "train.show()"
1834 | ]
1835 | },
1836 | {
1837 | "cell_type": "markdown",
1838 | "metadata": {},
1839 | "source": [
1840 | "### Perform machine learning model training (RandomForest)\n",
1841 | "\n",
1842 | "First we have to install the [r.learn.ml2](https://grass.osgeo.org/grass-stable/manuals/addons/r.learn.ml2.html) extention. It consists of two modules: `r.learn.train` and `r.learn.predict`."
1843 | ]
1844 | },
1845 | {
1846 | "cell_type": "code",
1847 | "execution_count": null,
1848 | "metadata": {},
1849 | "outputs": [],
1850 | "source": [
1851 | "# Install ML extension\n",
1852 | "gs.run_command(\"g.extension\", extension=\"r.learn.ml2\")"
1853 | ]
1854 | },
1855 | {
1856 | "cell_type": "code",
1857 | "execution_count": null,
1858 | "metadata": {},
1859 | "outputs": [],
1860 | "source": [
1861 | "# Add segmentation map created above to group and subgroup already populated with S2 bands, NDWI and NDVI\n",
1862 | "gs.run_command(\"i.group\", \n",
1863 | " group=\"s2\", \n",
1864 | " subgroup=\"s2\", \n",
1865 | " input=\"sentinel_segments_min100\")\n",
1866 | "\n",
1867 | "# List group content\n",
1868 | "print(gs.read_command(\"i.group\", group=\"s2\", flags=\"l\"))"
1869 | ]
1870 | },
1871 | {
1872 | "cell_type": "markdown",
1873 | "metadata": {},
1874 | "source": [
1875 | "We now train the ML model using [r.learn.train](https://grass.osgeo.org/grass-stable/manuals/addons/r.learn.train.html), with model \"RandomForestClassifier\"."
1876 | ]
1877 | },
1878 | {
1879 | "cell_type": "code",
1880 | "execution_count": null,
1881 | "metadata": {},
1882 | "outputs": [],
1883 | "source": [
1884 | "# Train a random forest classification model using r.learn.train\n",
1885 | "gs.run_command(\"r.learn.train\", \n",
1886 | " group=\"s2\", \n",
1887 | " training_map=\"landuse_train\",\n",
1888 | " model_name=\"RandomForestClassifier\",\n",
1889 | " n_estimators=\"500\", \n",
1890 | " save_model=os.path.join(homedir, \"rf_model.gz\"))"
1891 | ]
1892 | },
1893 | {
1894 | "cell_type": "markdown",
1895 | "metadata": {},
1896 | "source": [
1897 | " The model has been stored in the file `rf_model.gz` for use in the prediction step of the supervised classification."
1898 | ]
1899 | },
1900 | {
1901 | "cell_type": "code",
1902 | "execution_count": null,
1903 | "metadata": {},
1904 | "outputs": [],
1905 | "source": [
1906 | "os.listdir(homedir)"
1907 | ]
1908 | },
1909 | {
1910 | "cell_type": "markdown",
1911 | "metadata": {},
1912 | "source": [
1913 | "### Perform ML supervised classification\n",
1914 | "\n",
1915 | "The trained model will now be applied to the entire dataset."
1916 | ]
1917 | },
1918 | {
1919 | "cell_type": "code",
1920 | "execution_count": null,
1921 | "metadata": {},
1922 | "outputs": [],
1923 | "source": [
1924 | "# Perform prediction using r.learn.predict\n",
1925 | "gs.run_command(\"r.learn.predict\", \n",
1926 | " group=\"s2\", \n",
1927 | " load_model=os.path.join(homedir, \"rf_model.gz\"), \n",
1928 | " output=\"sentinel_rf\")"
1929 | ]
1930 | },
1931 | {
1932 | "cell_type": "code",
1933 | "execution_count": null,
1934 | "metadata": {},
1935 | "outputs": [],
1936 | "source": [
1937 | "# Set color table, we transfer the colors from the original landuse map\n",
1938 | "gs.run_command(\"r.colors\", map=\"sentinel_rf\", raster=\"nc_nlcd2019\")"
1939 | ]
1940 | },
1941 | {
1942 | "cell_type": "markdown",
1943 | "metadata": {},
1944 | "source": [
1945 | "With this, the (oversimplified) supervised classification has been completed and we can display the result."
1946 | ]
1947 | },
1948 | {
1949 | "cell_type": "markdown",
1950 | "metadata": {},
1951 | "source": [
1952 | "### Reporting and display"
1953 | ]
1954 | },
1955 | {
1956 | "cell_type": "code",
1957 | "execution_count": null,
1958 | "metadata": {},
1959 | "outputs": [],
1960 | "source": [
1961 | "# Display newly created sentinel_rf map\n",
1962 | "rfmap = gj.InteractiveMap(width = 600, tiles=\"OpenStreetMap\")\n",
1963 | "rfmap.add_raster(\"sentinel_rf\", opacity=0.7)\n",
1964 | "# rfmap.add_raster(\"nc_nlcd2019\", opacity=0.7)\n",
1965 | "rfmap.add_layer_control(position = \"bottomright\")\n",
1966 | "rfmap.show()"
1967 | ]
1968 | },
1969 | {
1970 | "cell_type": "code",
1971 | "execution_count": null,
1972 | "metadata": {},
1973 | "outputs": [],
1974 | "source": [
1975 | "# Show legend\n",
1976 | "legend = gj.Map(width=400, use_region=True)\n",
1977 | "legend.d_legend(raster=\"sentinel_rf\", \n",
1978 | " title=\"Classes\",\n",
1979 | " fontsize=14, \n",
1980 | " at=(10, 80, 10, 40), \n",
1981 | " flags=\"n\")\n",
1982 | "legend.show()"
1983 | ]
1984 | },
1985 | {
1986 | "cell_type": "code",
1987 | "execution_count": null,
1988 | "metadata": {},
1989 | "outputs": [],
1990 | "source": [
1991 | "# Show class distribution in percent\n",
1992 | "print(gs.read_command(\"r.report\", \n",
1993 | " map=\"sentinel_rf\", \n",
1994 | " units=\"p\", \n",
1995 | " flags=\"h\"))"
1996 | ]
1997 | },
1998 | {
1999 | "cell_type": "code",
2000 | "execution_count": null,
2001 | "metadata": {},
2002 | "outputs": [],
2003 | "source": [
2004 | "# export map to COG\n",
2005 | "gs.run_command(\"r.out.gdal\", \n",
2006 | " flags=\"fmt\", #\n",
2007 | " input=\"sentinel_rf\", \n",
2008 | " output=os.path.join(homedir, \"nc_sentinel2_RF.tif\"),\n",
2009 | " format=\"COG\", \n",
2010 | " overviews=\"4\")"
2011 | ]
2012 | },
2013 | {
2014 | "cell_type": "markdown",
2015 | "metadata": {},
2016 | "source": [
2017 | "Keep in mind, this classification was just a simplified example to show how the procedure works.\n",
2018 | "\n",
2019 | "At this moment you should use [r.kappa](https://grass.osgeo.org/grass-stable/manuals/r.kappa.html) to calculate accuracy of classification. As this step would require either field observation data or manual interpretation of the scene, we'll leave this as an exercise to do at home."
2020 | ]
2021 | },
2022 | {
2023 | "cell_type": "code",
2024 | "execution_count": null,
2025 | "metadata": {},
2026 | "outputs": [],
2027 | "source": [
2028 | "# Open the tif in QGIS, adapt path accordingly\n",
2029 | "!qgis $homedir/nc_sentinel2_RF.tif"
2030 | ]
2031 | },
2032 | {
2033 | "cell_type": "markdown",
2034 | "metadata": {},
2035 | "source": [
2036 | "## 14. Supervised Classification: Maximum Likelihood\n",
2037 | "\n",
2038 | "We will now demonstrate the workflow to perform a supervised maximum likelihood classification which is integrated with the semantic labels metadata class, and hence allow us to use the same spectral signature to classify multiple scenes as long as the raster map order in the group is the same."
2039 | ]
2040 | },
2041 | {
2042 | "cell_type": "markdown",
2043 | "metadata": {},
2044 | "source": [
2045 | "Let's first check the semantic labels of the bands in our `s2` group:"
2046 | ]
2047 | },
2048 | {
2049 | "cell_type": "code",
2050 | "execution_count": null,
2051 | "metadata": {},
2052 | "outputs": [],
2053 | "source": [
2054 | "band_list = gs.read_command(\"i.group\", group=\"s2\", flags=\"lg\")"
2055 | ]
2056 | },
2057 | {
2058 | "cell_type": "code",
2059 | "execution_count": null,
2060 | "metadata": {},
2061 | "outputs": [],
2062 | "source": [
2063 | "# Add semantic label to the segmentation\n",
2064 | "gs.run_command(\"r.support\", \n",
2065 | " map=\"sentinel_segments_min100\", \n",
2066 | " semantic_label=\"S2_seg\")"
2067 | ]
2068 | },
2069 | {
2070 | "cell_type": "code",
2071 | "execution_count": null,
2072 | "metadata": {},
2073 | "outputs": [],
2074 | "source": [
2075 | "for m in band_list.split():\n",
2076 | " sl = gs.raster_info(m)['semantic_label']\n",
2077 | " print(m,sl)"
2078 | ]
2079 | },
2080 | {
2081 | "cell_type": "markdown",
2082 | "metadata": {},
2083 | "source": [
2084 | "Now, we generate the signature file based on the training sample that we obtained earlier, this will then be the input for the maximum likelihood classification"
2085 | ]
2086 | },
2087 | {
2088 | "cell_type": "code",
2089 | "execution_count": null,
2090 | "metadata": {},
2091 | "outputs": [],
2092 | "source": [
2093 | "# obtain signature files\n",
2094 | "gs.run_command(\"i.gensig\", \n",
2095 | " trainingmap=\"landuse_train\", \n",
2096 | " group=\"s2\", \n",
2097 | " subgroup=\"s2\", \n",
2098 | " signaturefile=\"sig_sentinel\")"
2099 | ]
2100 | },
2101 | {
2102 | "cell_type": "code",
2103 | "execution_count": null,
2104 | "metadata": {},
2105 | "outputs": [],
2106 | "source": [
2107 | "# perform ML supervised classification\n",
2108 | "gs.run_command(\"i.maxlik\", \n",
2109 | " group=\"s2\", \n",
2110 | " subgroup=\"s2\", \n",
2111 | " signaturefile=\"sig_sentinel\", \n",
2112 | " output=\"sentinel_maxlik\")"
2113 | ]
2114 | },
2115 | {
2116 | "cell_type": "code",
2117 | "execution_count": null,
2118 | "metadata": {},
2119 | "outputs": [],
2120 | "source": [
2121 | "# check classes\n",
2122 | "print(gs.read_command(\"r.category\", \n",
2123 | " map=\"sentinel_maxlik\", \n",
2124 | " separator=\"comma\"))"
2125 | ]
2126 | },
2127 | {
2128 | "cell_type": "markdown",
2129 | "metadata": {},
2130 | "source": [
2131 | "In GRASS 8.2+, [i.maxlik](https://grass.osgeo.org/grass-stable/manuals/i.maxlik.html) classifier does not preserve the original class values in the output. Thus, here is a lookup-table for original class numbers and new category values:\n",
2132 | "\n",
2133 | "class|nlcd_class|landuse|RGB\n",
2134 | "--- | --- | --- | --- \n",
2135 | "1|11|Open Water|072:109:162\n",
2136 | "2|21|Developed, Open Space|225:205:206\n",
2137 | "3|22|Developed, Low Intensity|220:152:129\n",
2138 | "4|23|Developed, Medium Intensity|241:001:000\n",
2139 | "5|24|Developed, High Intensity|171:001:001\n",
2140 | "6|41|Deciduous Forest|108:169:102\n",
2141 | "7|42|Evergreen Forest|029:101:051\n",
2142 | "8|43|Mixed Forest|189:204:147\n",
2143 | "9|81|Hay/Pasture|221:216:062\n",
2144 | "10|90|Woody Wetlands|187:215:237"
2145 | ]
2146 | },
2147 | {
2148 | "cell_type": "code",
2149 | "execution_count": null,
2150 | "metadata": {},
2151 | "outputs": [],
2152 | "source": [
2153 | "# Set color table\n",
2154 | "colours = [\"1 072:109:162\", \"2 225:205:206\", \"3 220:152:129\", \"4 241:001:000\", \"5 171:001:001\", \"6 108:169:102\", \"7 029:101:051\", \"8 189:204:147\", \"9 221:216:062\", \"10 187:215:237\"]\n",
2155 | "gs.write_command(\"r.colors\", map=\"sentinel_maxlik\", rules=\"-\", stdin=\"\\n\".join(colours))"
2156 | ]
2157 | },
2158 | {
2159 | "cell_type": "code",
2160 | "execution_count": null,
2161 | "metadata": {},
2162 | "outputs": [],
2163 | "source": [
2164 | "# display results\n",
2165 | "maxlik_sup_class = gj.Map(width=500, use_region=True)\n",
2166 | "maxlik_sup_class.d_rast(map=\"sentinel_maxlik\")\n",
2167 | "maxlik_sup_class.d_legend(raster=\"sentinel_maxlik\", \n",
2168 | " title=\"Class\", \n",
2169 | " fontsize=12, \n",
2170 | " at=(70, 95, 75, 90), \n",
2171 | " flags=\"bn\")\n",
2172 | "maxlik_sup_class.d_barscale()\n",
2173 | "maxlik_sup_class.show()"
2174 | ]
2175 | },
2176 | {
2177 | "cell_type": "code",
2178 | "execution_count": null,
2179 | "metadata": {},
2180 | "outputs": [],
2181 | "source": [
2182 | "# percentage of each class\n",
2183 | "print(gs.read_command(\"r.report\", \n",
2184 | " map=\"sentinel_maxlik\", \n",
2185 | " units=\"p\", \n",
2186 | " flags=\"h\"))"
2187 | ]
2188 | },
2189 | {
2190 | "cell_type": "code",
2191 | "execution_count": null,
2192 | "metadata": {},
2193 | "outputs": [],
2194 | "source": [
2195 | "# class statistics: NDVI\n",
2196 | "class_stats = gs.read_command(\"r.univar\", \n",
2197 | " map=\"T17SQV_20220528T155819_NDVI_10m\", \n",
2198 | " zones=\"sentinel_maxlik\", \n",
2199 | " flags=\"t\")"
2200 | ]
2201 | },
2202 | {
2203 | "cell_type": "code",
2204 | "execution_count": null,
2205 | "metadata": {},
2206 | "outputs": [],
2207 | "source": [
2208 | "pd.read_csv(StringIO(class_stats), \n",
2209 | " delimiter=\"|\", \n",
2210 | " usecols=[1, 4, 5, 7])"
2211 | ]
2212 | },
2213 | {
2214 | "cell_type": "markdown",
2215 | "metadata": {},
2216 | "source": [
2217 | "Next, and to demonstrate the use of semantic labels, we will classify another sentinel scene with the same signature obtained earlier. To this aim, we need to:\n",
2218 | "1. create a new imagery group for a different scene with the exact same band order\n",
2219 | "1. estimate NDVI and NDWI and assign semantic labels\n",
2220 | "1. run a segmentation and assign semantic labels\n",
2221 | "1. check group and semantic labels\n",
2222 | "1. run `i.maxlik`\n",
2223 | "\n",
2224 | "
\n",
2225 | "Be ware – changes over time (phenology, weather) will make spectral signatures to not fit well or at all. Do not use same signatures for a different season!"
2226 | ]
2227 | },
2228 | {
2229 | "cell_type": "code",
2230 | "execution_count": null,
2231 | "metadata": {},
2232 | "outputs": [],
2233 | "source": [
2234 | "s2_maps = gs.list_grouped(type=\"raster\", pattern=\"*20220617*\")['sentinel2']\n",
2235 | "s2_maps"
2236 | ]
2237 | },
2238 | {
2239 | "cell_type": "code",
2240 | "execution_count": null,
2241 | "metadata": {},
2242 | "outputs": [],
2243 | "source": [
2244 | "# Since imagery groups can not be overwritten, \n",
2245 | "# we delete any leftover \"s2_new\" group from previous runs\n",
2246 | "gs.run_command(\"g.remove\", \n",
2247 | " type=\"group\", \n",
2248 | " name=\"s2_new\", \n",
2249 | " flags=\"f\")"
2250 | ]
2251 | },
2252 | {
2253 | "cell_type": "code",
2254 | "execution_count": null,
2255 | "metadata": {},
2256 | "outputs": [],
2257 | "source": [
2258 | "gs.run_command(\"i.group\", group=\"s2_new\", subgroup=\"s2_new\", input=s2_maps)\n",
2259 | "print(gs.read_command(\"i.group\", group=\"s2_new\", flags=\"l\"))"
2260 | ]
2261 | },
2262 | {
2263 | "cell_type": "code",
2264 | "execution_count": null,
2265 | "metadata": {},
2266 | "outputs": [],
2267 | "source": [
2268 | "# estimate NDVI\n",
2269 | "gs.run_command(\"i.vi\", \n",
2270 | " red=\"T17SQV_20220617T155829_B04_10m\", \n",
2271 | " nir=\"T17SQV_20220617T155829_B08_10m\", \n",
2272 | " output=\"T17SQV_20220617T155829_NDVI_10m\", \n",
2273 | " viname=\"ndvi\")\n",
2274 | "\n",
2275 | "# add semantic label\n",
2276 | "gs.run_command(\"r.support\", \n",
2277 | " map=\"T17SQV_20220617T155829_NDVI_10m\", \n",
2278 | " semantic_label=\"S2_NDVI\")"
2279 | ]
2280 | },
2281 | {
2282 | "cell_type": "code",
2283 | "execution_count": null,
2284 | "metadata": {},
2285 | "outputs": [],
2286 | "source": [
2287 | "# estimate NDWI\n",
2288 | "gs.run_command(\"i.wi\", \n",
2289 | " green=\"T17SQV_20220617T155829_B03_10m\", \n",
2290 | " nir=\"T17SQV_20220617T155829_B08_10m\", \n",
2291 | " output=\"T17SQV_20220617T155829_NDWI_10m\", \n",
2292 | " winame=\"ndwi_mf\")\n",
2293 | "\n",
2294 | "# add semantic label\n",
2295 | "gs.run_command(\"r.support\", \n",
2296 | " map=\"T17SQV_20220617T155829_NDWI_10m\", \n",
2297 | " semantic_label=\"S2_NDWI\")"
2298 | ]
2299 | },
2300 | {
2301 | "cell_type": "code",
2302 | "execution_count": null,
2303 | "metadata": {},
2304 | "outputs": [],
2305 | "source": [
2306 | "# add NDVI and NDWI to s2_mew group\n",
2307 | "gs.run_command(\"i.group\", \n",
2308 | " group=\"s2_new\", \n",
2309 | " subgroup=\"s2_new\", \n",
2310 | " input=\"T17SQV_20220617T155829_NDVI_10m,T17SQV_20220617T155829_NDWI_10m\")\n",
2311 | "\n",
2312 | "# print maps in the group\n",
2313 | "print(gs.read_command(\"i.group\", group=\"s2_new\", flags=\"l\"))"
2314 | ]
2315 | },
2316 | {
2317 | "cell_type": "code",
2318 | "execution_count": null,
2319 | "metadata": {},
2320 | "outputs": [],
2321 | "source": [
2322 | "# Run segmentation\n",
2323 | "gs.run_command(\"i.segment\", \n",
2324 | " group=\"s2_new\", \n",
2325 | " threshold=\"0.05\", \n",
2326 | " minsize=\"100\", \n",
2327 | " output=\"sentinel_new_segments_min100\", \n",
2328 | " goodness=\"sentinel_new_segments_goodness_min100\")"
2329 | ]
2330 | },
2331 | {
2332 | "cell_type": "code",
2333 | "execution_count": null,
2334 | "metadata": {},
2335 | "outputs": [],
2336 | "source": [
2337 | "# Add semantic label to the segmentation\n",
2338 | "gs.run_command(\"r.support\", \n",
2339 | " map=\"sentinel_new_segments_min100\", \n",
2340 | " semantic_label=\"S2_seg\")"
2341 | ]
2342 | },
2343 | {
2344 | "cell_type": "code",
2345 | "execution_count": null,
2346 | "metadata": {},
2347 | "outputs": [],
2348 | "source": [
2349 | "# Add segmentation to the s2_new group\n",
2350 | "gs.parse_command(\"i.group\", group=\"s2_new\", subgroup=\"s2_new\", input=\"sentinel_new_segments_min100\")"
2351 | ]
2352 | },
2353 | {
2354 | "cell_type": "code",
2355 | "execution_count": null,
2356 | "metadata": {},
2357 | "outputs": [],
2358 | "source": [
2359 | "# Check\n",
2360 | "print(gs.read_command(\"i.group\", group=\"s2_new\", flags=\"l\"))"
2361 | ]
2362 | },
2363 | {
2364 | "cell_type": "code",
2365 | "execution_count": null,
2366 | "metadata": {},
2367 | "outputs": [],
2368 | "source": [
2369 | "# Run the classification\n",
2370 | "gs.run_command(\"i.maxlik\", \n",
2371 | " group=\"s2_new\", \n",
2372 | " subgroup=\"s2_new\", \n",
2373 | " signaturefile=\"sig_sentinel\", \n",
2374 | " output=\"sentinel_maxlik_new\")"
2375 | ]
2376 | },
2377 | {
2378 | "cell_type": "code",
2379 | "execution_count": null,
2380 | "metadata": {},
2381 | "outputs": [],
2382 | "source": [
2383 | "# Set color table\n",
2384 | "colours = [\"1 072:109:162\", \"2 225:205:206\", \"3 220:152:129\", \"4 241:001:000\", \"5 171:001:001\", \"6 108:169:102\", \"7 029:101:051\", \"8 189:204:147\", \"9 221:216:062\", \"10 187:215:237\"]\n",
2385 | "gs.write_command(\"r.colors\", map=\"sentinel_maxlik_new\", rules=\"-\", stdin=\"\\n\".join(colours))"
2386 | ]
2387 | },
2388 | {
2389 | "cell_type": "code",
2390 | "execution_count": null,
2391 | "metadata": {},
2392 | "outputs": [],
2393 | "source": [
2394 | "# display results\n",
2395 | "maxlik_sup_class = gj.Map(width=500, use_region=True)\n",
2396 | "maxlik_sup_class.d_rast(map=\"sentinel_maxlik_new\")\n",
2397 | "maxlik_sup_class.d_legend(raster=\"sentinel_maxlik_new\", \n",
2398 | " title=\"Class\", \n",
2399 | " fontsize=12, \n",
2400 | " at=(60, 95, 70, 90), \n",
2401 | " flags=\"bn\")\n",
2402 | "maxlik_sup_class.d_barscale()\n",
2403 | "maxlik_sup_class.show()"
2404 | ]
2405 | },
2406 | {
2407 | "cell_type": "markdown",
2408 | "metadata": {},
2409 | "source": [
2410 | "## 15. What's next?\n",
2411 | "\n",
2412 | "You may enjoy more Jupyter notebooks at: https://github.com/OSGeo/grass/tree/main/doc/notebooks\n",
2413 | "\n",
2414 | "### Talk to us\n",
2415 | "\n",
2416 | "- Veronica Andreo, PhD, https://veroandreo.gitlab.io/\n",
2417 | "- Markus Neteler, PhD, https://www.mundialis.de/en/neteler/\n",
2418 | "- Māris Nartišs, PhD"
2419 | ]
2420 | },
2421 | {
2422 | "cell_type": "markdown",
2423 | "metadata": {},
2424 | "source": [
2425 | "### References\n",
2426 | "\n",
2427 | "- [GRASS GIS 8.2.0 Reference Manual](https://grass.osgeo.org/grass-stable/manuals/)\n",
2428 | "- [GRASS GIS Addons Reference Manuals](https://grass.osgeo.org/grass-stable/manuals/addons/)\n",
2429 | "- [GRASS GIS Python library documentation](https://grass.osgeo.org/grass-stable/manuals/libpython/)\n",
2430 | "- [Unleash the power of GRASS GIS with Jupyter](https://github.com/ncsu-geoforall-lab/grass-gis-workshop-foss4g-2022)\n",
2431 | "- List of [Tutorials](https://grass.osgeo.org/learn/tutorials/) at the GRASS GIS website"
2432 | ]
2433 | }
2434 | ],
2435 | "metadata": {
2436 | "kernelspec": {
2437 | "display_name": "Python 3 (ipykernel)",
2438 | "language": "python",
2439 | "name": "python3"
2440 | },
2441 | "language_info": {
2442 | "codemirror_mode": {
2443 | "name": "ipython",
2444 | "version": 3
2445 | },
2446 | "file_extension": ".py",
2447 | "mimetype": "text/x-python",
2448 | "name": "python",
2449 | "nbconvert_exporter": "python",
2450 | "pygments_lexer": "ipython3",
2451 | "version": "3.10.5"
2452 | }
2453 | },
2454 | "nbformat": 4,
2455 | "nbformat_minor": 4
2456 | }
2457 |
--------------------------------------------------------------------------------