diff --git a/src/imagery/i.saocom/Makefile b/src/imagery/i.saocom/Makefile new file mode 100644 index 0000000000..88e7c34f58 --- /dev/null +++ b/src/imagery/i.saocom/Makefile @@ -0,0 +1,13 @@ +MODULE_TOPDIR =../.. + +PGM = i.saocom + +SUBDIRS = i.saocom.geocode \ + i.saocom.import \ + +include $(MODULE_TOPDIR)/include/Make/Dir.make + +default: parsubdirs htmldir + +install: installsubdirs + $(INSTALL_DATA) $(PGM).html $(INST_DIR)/docs/html/ diff --git a/src/imagery/i.saocom/i.saocom.geocode/Makefile b/src/imagery/i.saocom/i.saocom.geocode/Makefile new file mode 100755 index 0000000000..039f345242 --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.geocode/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = i.saocom.geocode + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/imagery/i.saocom/i.saocom.geocode/i.saocom.geocode.html b/src/imagery/i.saocom/i.saocom.geocode/i.saocom.geocode.html new file mode 100755 index 0000000000..241b662759 --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.geocode/i.saocom.geocode.html @@ -0,0 +1,52 @@ +

DESCRIPTION

+ +The i.saocom.geocode module allows the projection of SAOCOM-1 L1A raw or derived products from radar coordinates to a cartographic coordinate system (CS). This can be applied to bands imported by i.saocom.import, any raster map calculated from these inputs, or directly to the SAOCOM original files. The tool does not currently support the geocoding of image subsets, so the whole extent must be processed. + +

+

+ +

Graphical description of the workflow followed by i.saocom.geocode +

+ +

NOTES

+ +When the tool is directly run on the original SAOCOM files, it will internally call i.saocom.import to import the real and imaginary bands to a temporary XY unprojected location. In this case the data option must be used. If the user wants to geocode already imported files, the map option must be specified. The tool will not allow the use of both options, either one of them must be selected. + +This module makes use of the GCPS.csv file generated by i.saocom.import and associated to a specific image. This GCP file is associated to the image basename specified in the option basename. + +The tool will write the output to a Geotiff external file in the directory specified by the dbase option. The output files will be projected to the CS of the target location indicated in the option location. If the specified location does not exist, GRASS will create it with the CS EPSG:4326. The output map name will be the same as the input map, with the suffix _geo added at the end. + + +

EXAMPLES

+ +

Within an XY unprojected location where the SAOCOM-1 bands have already been imported and processed, create an amplitude image from the real and imaginary bands, geocode it and then import it into a location called saocom_geo

+ +
+#Calculate the amplitude image 
+i.sar.amplitude real=SAO1B_20211222_hh_real imag=SAO1B_20211222_hh_imag output=SAO1B_20211222_hh_amp
+
+#Geocode it and import it to new location. The external temporary raster will be saved as GeoTiff in the $HOME directory
+i.saocom.geocode map=SAO1B_20211222_hh_amp basename=SAO1B_20211222 dbase=$HOME location=saocom_geo
+
+ +

Geocode the real and imaginary bands of a SAOCOM L1A image and import them to a location called geo_location

+ +
+zip_file = 'S1B_OPER_SAR_EOSSP__CORE_L1A_OLF_20211225T165228.zip'
+i.saocom.geocode data=zip_file pols=['hh'] multilook=[8,4] basename='SAO1B_20211222' location='geo_location' dbase ='$HOME' )
+
+ +The previous example will run i.saocom.import to generate the real and imaginary bands for the polarimetric channels specified in the pols option and will apply +the multilook factor indicated in the multilook option. After that it will geocode the real and imaginary bands and import them to the target location. + + +

SEE ALSO

+ +i.saocom.import, +i.sar.amplitude, +i.sar.speckle, +i.sar.pauliRGB + +

AUTHORS

+ +Santiago Seppi, CONAE, Argentina. diff --git a/src/imagery/i.saocom/i.saocom.geocode/i.saocom.geocode.py b/src/imagery/i.saocom/i.saocom.geocode/i.saocom.geocode.py new file mode 100755 index 0000000000..5a00b5e627 --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.geocode/i.saocom.geocode.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: i.saocom.geocode +# +# AUTHOR: Santiago Seppi +# +# PURPOSE: Geocode SAOCOM SLC derived products using information from Ground Control Points (GCPs). The program writes the geocoded grid to a temporary external file, and then imports it to a new location and mapset. +# +# COPYRIGHT: (C) 2002-2023 by the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +# +############################################################################# + +# %Module +# % description: Geocode SAOCOM SLC derived products +# % keyword: imagery +# % keyword: saocom +# % keyword: sar +# % keyword: radar +# % overwrite: yes +# %End +# %option G_OPT_R_INPUT +# % key: map +# % required: no +# % description: Map to be geocoded, in radar coordinates +# %end +# %option +# % key: data +# % type: string +# % required: no +# % multiple: no +# % description: Path to data directory (ZIP or folder) +# %end +# %option +# % key: is_zip +# % type: string +# % required: no +# % multiple: no +# % answer: yes +# % options: yes,no +# % description: Whether the data directory is zipped or not. Only required if geocoding is to ble applied on the original files +# %end +# %option +# % key: polarizations +# % type: string +# % required: no +# % multiple: yes +# % answer: ['hh','hv','vh','vv'] +# % description: Polarizations to process . Only required if geocoding is to ble applied on the original files. (Default:['hh','hv','vh','vv']) +# %end +# %option +# % key: multilook +# % type: integer +# % required: no +# % multiple: yes +# % answer: 1,1 +# % description: Azimuth and range factors to apply (eg: [4,2])). Only required if geocoding is to be applied on the original files +# %end +# %option +# % key: basename +# % type: string +# % required: yes +# % multiple: no +# % description: Basename folder contaning the GCPS info. This folder is generated by i.saocom.import +# %end +# %option +# % key: location +# % type: string +# % multiple: no +# % required: yes +# % description: Location where the geocoded map will be stored +# %end +# %option G_OPT_M_DIR +# % key: dbase +# % multiple: no +# % required: yes +# % description: GRASS GIS database directory to store the external geocoded map +# %end + + +import os +import numpy as np +import grass.script as gs +import rasterio +import numpy as np +from osgeo import gdal +import pandas as pd +import shutil + + +def save_GeoTiff(fn, crs, transform, mat, meta=None, nodata=None, bandnames=[]): + if len(mat.shape) == 2: + count = 1 + else: + count = mat.shape[0] + + if not meta: + meta = {} + + meta["driver"] = "GTiff" + meta["height"] = mat.shape[-2] + meta["width"] = mat.shape[-1] + meta["count"] = count + meta["crs"] = crs + meta["transform"] = transform + + if "dtype" not in meta: # if no datatype is specified, use float32 + meta["dtype"] = np.float32 + + if nodata is None: + pass + else: + meta["nodata"] = nodata + + with rasterio.open(fn, "w", **meta) as dst: + if count == 1: # Mono-band raster + dst.write(mat.astype(meta["dtype"]), 1) + if bandnames: + dst.set_band_description(1, bandnames[0]) + else: # Multi-band raster + for b in range(count): + dst.write(mat[b].astype(meta["dtype"]), b + 1) + for b, bandname in enumerate(bandnames): + dst.set_band_description(b + 1, bandname) + + +def read_gcps(gcp_base_folder): + df_gcps = pd.read_csv(os.path.join(gcp_base_folder, "GCPS.csv")) + gcps = [] + for i, d in df_gcps.iterrows(): + gcp = rasterio.control.GroundControlPoint( + d.row, d.col, d.x, d.y, d.z, d.id, d.info + ) + gcps.append(gcp) + return gcps + + +def geocode_file(input_map, basename, outdir, output_map, format="GTiff"): + # Write the map to Geotiff file + gs.run_command( + "r.out.gdal", + input=input_map, + output=os.path.join(outdir, output_map), + format="GTiff", + ) + # Read as rasterio dataset + ds = rasterio.open(os.path.join(outdir, output_map)) + ds = ds.read()[0] + # Read the GCPS dataframe + env = gs.gisenv() + gcp_base_folder = os.path.join( + env["GISDBASE"], env["LOCATION_NAME"], env["MAPSET"], "cell_misc", basename + ) + gcps = read_gcps(gcp_base_folder) + # Save the geocoded raster (it will replace the previous intermediate file) + geoTs = rasterio.transform.from_gcps(gcps) + crs = rasterio.crs.CRS.from_epsg(4326) + save_GeoTiff(fn=os.path.join(outdir, output_map), crs=crs, transform=geoTs, mat=ds) + + +def export_to_location(outdir, location, input_map, int_map, env): + # Run gdalWarp. This is made to avoid ERROR: Input map is rotated - cannot import + output_warp = f"gdalwarp_{int_map}" + os.system( + f"gdalwarp {os.path.join(outdir,int_map)} {os.path.join(outdir,output_warp)}" + ) + + gs.warning(_("Switching location")) + + # Create the new location with EPSG:4326, in case it does not exist + location_folder = env["GISDBASE"] + out_location = os.path.join(location_folder, location) + if not os.path.exists(out_location): + gs.create_location( + env["GISDBASE"], + location, + epsg=4326, + desc="Location created by i.saocom.geocode", + ) + + gs.run_command("g.mapset", mapset="PERMANENT", location=location) + gs.run_command( + "r.import", input=os.path.join(outdir, output_warp), output=f"{input_map}" + ) + os.remove(os.path.join(outdir, output_warp)) + + +def main(): + input_map = options["map"] + data = options["data"] + zip_v = options["is_zip"] + pols = options["polarizations"] + multilook = options["multilook"] + basename = options["basename"] + location = options["location"] + outdir = options["dbase"] + env = gs.gisenv() + + if not input_map and not data: + gs.fatal(_("Either one of input map or data folder/zip must be specified")) + + if input_map and data: + gs.fatal( + _("Either one of input map or data folder/zip must be specified, not both") + ) + + if not input_map and data: + # Import real and imaginary bands to a temporary location and geocode them to external file + gs.message(_("Running i.saocom.import")) + gs.create_location( + env["GISDBASE"], f"{basename}_XY_tempLocation", overwrite=1 + ) + gs.run_command( + "g.mapset", mapset="PERMANENT", location=f"{basename}_XY_tempLocation" + ) + gs.run_command( + "i.saocom.import", + data=data, + is_zip=zip_v, + pols=pols, + multilook=multilook, + basename=basename, + ) + # Get the list of maps to be geocoded + map_list = gs.list_grouped(type="raster", pattern=f"{basename}*")[ + "PERMANENT" + ] + for m in map_list: + gs.run_command("g.region", raster=m) + geocode_file( + input_map=m, + basename=basename, + outdir=outdir, + output_map=f"{m}.tif", + format="GTiff", + ) + # Import the geocoded bands into the target location + export_to_location( + outdir=outdir, + location=location, + input_map=f"{m}_geo", + int_map=f"{m}.tif", + env=env, + ) + # Remove intermediate files + os.remove(os.path.join(outdir, f"{m}.tif")) + # Go back to temporary location to continue exporting + gs.run_command( + "g.mapset", mapset="PERMANENT", location=f"{basename}_XY_tempLocation" + ) + # Go back to original location + gs.run_command( + "g.mapset", mapset=env["MAPSET"], location=env["LOCATION_NAME"] + ) + shutil.rmtree(os.path.join(env["GISDBASE"], f"{basename}_XY_tempLocation")) + + if input_map and not data: + # Check if this an XY location + proj = gs.read_command("g.proj", flags="g").split("=")[1].split("\n")[0] + if proj != "xy_location_unprojected": + raise ValueError( + "Current location is not XY unprojected (radar coordinates)" + ) + geocode_file( + input_map=input_map, + basename=basename, + outdir=outdir, + output_map=f"{input_map}.tif", + format="GTiff", + ) + export_to_location( + outdir=outdir, + location=location, + input_map=f"{input_map}_geo", + int_map=f"{input_map}.tif", + env=env, + ) + # Remove intermediate files + os.remove(os.path.join(outdir, f"{input_map}.tif")) + # Go back to original location + gs.run_command( + "g.mapset", mapset=env["MAPSET"], location=env["LOCATION_NAME"] + ) + + +if __name__ == "__main__": + options, flags = gs.parser() + main() diff --git a/src/imagery/i.saocom/i.saocom.geocode/saocomGeocodeWF.png b/src/imagery/i.saocom/i.saocom.geocode/saocomGeocodeWF.png new file mode 100644 index 0000000000..c6f5edc0a9 Binary files /dev/null and b/src/imagery/i.saocom/i.saocom.geocode/saocomGeocodeWF.png differ diff --git a/src/imagery/i.saocom/i.saocom.html b/src/imagery/i.saocom/i.saocom.html new file mode 100644 index 0000000000..468b5fbd5b --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.html @@ -0,0 +1,41 @@ + + + +i.saocom toolset - GRASS GIS manual + + + + + +
+ +GRASS logo +
+ +

NAME

+ +i.saocom - Toolset to import and process SAOCOM-1 L1A products. + +

KEYWORDS

+imagery, +import, +satellite + + +

DESCRIPTION

+ +The i.saocom toolset consists of two modules: + +
+
i.saocom.import
+
Imports SAOCOM-1 data L1A (SLC) products
+
i.saocom.geocode
+
Projects SAOCOM-1 L1A derived products to a cartographic coordinate system and imports them to a new location
+
+ +

AUTHORS

+ +Santiago Seppi, CONAE, Argentina + +

SOURCE CODE

+ diff --git a/src/imagery/i.saocom/i.saocom.import/Makefile b/src/imagery/i.saocom/i.saocom.import/Makefile new file mode 100755 index 0000000000..ed77eec51a --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.import/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = i.saocom.import + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/imagery/i.saocom/i.saocom.import/i.saocom.import.html b/src/imagery/i.saocom/i.saocom.import/i.saocom.import.html new file mode 100755 index 0000000000..2ba68e7174 --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.import/i.saocom.import.html @@ -0,0 +1,71 @@ +

DESCRIPTION

+ +The i.saocom.import module allows importing SAOCOM-1 L1A (SLC) products from their native format, as provided by CONAE. +SLC products are complex matrices, but GRASS will import them as two separate bands containing the real and imaginary parts, which allows further manipulation and prevents from losing important information. +As described in the graphical workflow, most of the code works outside GRASS GIS: It temporarily reads the SLC data (either a folder or zip file), extracts the real and imaginary components, and saves them as GeoTiff files. +These GeoTiff files are then imported to GRASS GIS using r.import and stored as raster maps in the PERMANENT mapset. Additionaly, a Ground Control Points file +associated to these files will be created within de cell_misc directory, in case the user performs later geocoding with i.saocom.geocode. This GCP information is extracted from the original SAOCOM-1 matrix using Python rasterio library and will account for any multilooking applied to the images. + +

+By default i.saocom.import will process all the available polarizations, which can be selected with the polarizations option. +The module assumes by default that the data is zipped but this can be changed with the is_zip option. The user can also apply a multilooking factor, which must be indicated as a list, followinf the order [az_loos,rg_looks]. +By default no multilooking is applied. The module requires to specify a basename value which will be used as prefix for all the polarization bands to be imported. + + +

+Important: i.saocom.import assumes a GRASS session is active in an XY unprojected location. Importing the files to projected locations may lead to non-desired outputs. + + +

+ +

Graphical description of the workflow followed by i.saocom.import +

+ +

NOTES

+ +SAOCOM L1A images are projected in radar coordinates. To run this module it is important to previously create an XY (unprojected) location. +A file named GCPS.csv with the Ground Control Points information will be stored in the current location's cell_misc directory, inside a sub-folder that will be named after the basename value. + +

EXAMPLES

+ +

Import all polarizations

+ +Firstly, an unprojected (XY) location must be created, the example below creates a location called saocom: + +
+grass -c XY saocom -e
+
+ +Then, run i.saocom.import from a zip file containing polarizations = ['hh','hv','vh','vv']: + +
+zip_file = S1B_OPER_SAR_EOSSP__CORE_L1A_OLF_20211225T165228.zip
+i.saocom.import data=zip_file is_zip=yes basename=SAO1B_20211222
+
+ +The previous example will create two bands using the given basename as prefix: SAO1B_20211222_[pol]_r (real band) SAO1B_20211222_[pol]_i (imaginary band). [pol] +indicates the imported polarization, which for SAOCOM images can be any of ['hh','hv','vh','vv']. + +

Import specific polarizations and apply multilooking

+ +
+zip_file = S1B_OPER_SAR_EOSSP__CORE_L1A_OLF_20211225T165228.zip
+i.saocom.import data=zip_file is_zip =yes basename=SAO1B_20211222 polarizations=hh,vv multilook=4,2
+
+ +The previous example will import only the hh and vv vv polarizations, and in case none of them are found the program will let the user know which are the available polarization channels in the dataset. The multilooking factor will be of 4 in azimuth direction and 2 in range direction. + +

REFERENCES

+ +Reference or references to paper related to the tool or references which algorithm the tool is based on. + +

SEE ALSO

+ +i.saocom.geocode, +i.sar.amplitude, +i.sar.speckle, +i.sar.pauliRGB + +

AUTHORS

+ +Santiago Seppi, CONAE, Argentina. diff --git a/src/imagery/i.saocom/i.saocom.import/i.saocom.import.py b/src/imagery/i.saocom/i.saocom.import/i.saocom.import.py new file mode 100644 index 0000000000..ab4c038a95 --- /dev/null +++ b/src/imagery/i.saocom/i.saocom.import/i.saocom.import.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: i.sa +# +# AUTHOR: Santiago Seppi +# +# PURPOSE: Read SAOCOM SLC files into real and imaginary bands +# +# COPYRIGHT: (C) 2002-2023 by the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +# +############################################################################# + +# %Module +# % description: Read SAOCOM SLC file into GRASS as real and imaginary bands +# % keyword: imagery +# % keyword: saocom +# % keyword: sar +# % keyword: radar +# % overwrite: yes +# %End +# %option +# % key: data +# % type: string +# % required: yes +# % multiple: no +# % description: Path to data directory (ZIP or folder) +# %end +# %option +# % key: is_zip +# % type: string +# % required: yes +# % multiple: no +# % answer: yes +# % options: yes,no +# % description: Whether the data directory is zipped or not +# %end +# %option +# % key: polarizations +# % type: string +# % required: yes +# % multiple: yes +# % answer: ['hh','hv','vh','vv'] +# % description: Polarizations to process (Default:['hh','hv','vh','vv']) +# %end +# %option +# % key: multilook +# % type: integer +# % required: yes +# % multiple: yes +# % answer: 1,1 +# % description: Azimuth and range factors to apply (eg: [4,2])) +# %end +# %option G_OPT_R_OUTPUT +# % key: basename +# % description: Prefix for output raster map +# % required: yes +# %end + + +import os +import numpy as np +import grass.script as gs +from zipfile import ZipFile +import rasterio +import numpy as np +from osgeo import gdal +from affine import Affine +import pandas as pd + + +def apply_multilook(dataset, azLooks, rgLooks): + """ + Esta función aplica un multilook dado a una imagen de entrada y actualiza la información de los metadatos + para la imagen de salida. + + parámetros: + dataset: diccionario de datos con las llaves: + 'array': contiene la matriz de datos + 'metadata': contiene los metadatos con el formato dado por la librería rasterio + azLooks: número de looks a aplicar en dirección de azimuth + rgLooks: número de looks a aplicar en dirección de rango + + retorna: + diccionario de datos con la estructura del dataset de entrada pero con el array resultante del + multilook y los metadatos actualizados + """ + + + array = dataset["array"].copy() + metadata = dataset["metadata"].copy() + + array = array[0] + + cols = metadata["width"] + rows = metadata["height"] + new_rows = rows // azLooks + new_cols = cols // rgLooks + new_shape = (new_rows, azLooks, new_cols, rgLooks) + + if rows % azLooks != 0 or cols % rgLooks != 0: + array = array[: new_rows * azLooks, : new_cols * rgLooks] + + multilooked = array.reshape(new_shape).mean(-1).mean(1) + metadata["width"] = new_cols + metadata["height"] = new_rows + geoTs = metadata["transform"] + metadata["transform"] = Affine( + geoTs[0] * rgLooks, geoTs[1], geoTs[2], geoTs[3], geoTs[4] * azLooks, geoTs[5] + ) + + return {"array": np.expand_dims(multilooked, axis=0), "metadata": metadata} + + +def read_bands_zip(data, pols): + with ZipFile(data, "r") as zfile: + file_list = [ + img + for img in zfile.namelist() + if img.startswith("Data/") and not img.endswith(".xml") + ] + bands = {} + gcps = tuple() + dataset_pols = [] + for l in file_list: + pol = l.split("-")[6] + dataset_pols.append(pol) + if pol in pols: + bands[pol] = {} + filename = f"/vsizip/{data}/{l}" + with rasterio.open(filename) as banda: + bands[pol]["array"] = banda.read() + bands[pol]["metadata"] = banda.meta + gcps = banda.get_gcps() + return bands, gcps, dataset_pols + + +def read_bands_folder(data, pols): + file_list = [ + img + for img in os.listdir(os.path.join(data, "Data")) + if not img.endswith(".xml") + ] + bands = {} + gcps = tuple() + dataset_pols = [] + for l in file_list: + pol = l.split("-")[6] + dataset_pols.append(pol) + if pol in pols: + bands[pol] = {} + filename = os.path.join(data, "Data", l) + with rasterio.open(filename) as banda: + bands[pol]["array"] = banda.read() + bands[pol]["metadata"] = banda.meta + gcps = banda.get_gcps() + return bands, gcps, dataset_pols + + +def save_bands(bands, basename): + for band in bands: + # Change the band metadata: + # Data type must be changed from complex to float + bands[band]["metadata"]["dtype"] = np.float32 + # The Y-resolution must be set to negative, otherwise gs.will interpret the map is flipped + geoTs = bands[band]["metadata"]["transform"] + bands[band]["metadata"]["transform"] = Affine( + geoTs[0], geoTs[1], geoTs[2], geoTs[3], -geoTs[4], geoTs[5] + ) + outputfn_r = f"{basename}_{band}_real.tif" + outputfn_i = f"{basename}_{band}_imag.tif" + with rasterio.open(outputfn_r, "w", **bands[band]["metadata"]) as dst: + dst.write(bands[band]["array"].real) + with rasterio.open(outputfn_i, "w", **bands[band]["metadata"]) as dst: + dst.write(bands[band]["array"].imag) + + +def main(): + # xemt = options["xemt"] + data = options["data"] + zip_v = options["is_zip"] + basename = options["basename"] + pols = options["polarizations"] + multilook = options["multilook"] + + if zip_v == "yes": + bands, gcps, dataset_pols = read_bands_zip(data, pols) + else: + bands, gcps, dataset_pols = read_bands_folder(data, pols) + + if len(bands) == 0 or len(gcps) == 0: + gs.fatal( + _( + f"None of the specified polarizations were found in the dataset \n Please try one of the folowing: {dataset_pols}" + ) + ) + # pass + + else: + # Create a dataframe containing GCP information + cols = gcps[0][0].asdict().keys() + df = pd.DataFrame(columns=cols) + for i, gcp in enumerate(gcps[0]): + df1 = pd.DataFrame(gcp.asdict(), index=[i]) + df = pd.concat((df, df1)) + + if multilook[0] != 1 or multilook[2] != 1: + # ~ print(f'Appying ML factor: {multilook}') + gs.message(_(f"Appying ML factor: {multilook}")) + for band in bands: + dim = bands[band]["array"].shape + # ~ print('Original shape ', bands[band]['array'].shape) + gs.message(_(f"Original shape {dim}")) + ml_dic = apply_multilook( + bands[band], int(multilook[0]), int(multilook[2]) + ) + bands[band]["array"] = ml_dic["array"] + bands[band]["metadata"] = ml_dic["metadata"] + dim = bands[band]["array"].shape + # ~ print('Shape after ML', bands[band]['array'].shape) + gs.message(_(f"Shape after ML {dim}")) + # Update GCP information + # ~ print('Updating GCP information for later geocoding') + gs.message(_("Updating GCP information for later geocoding")) + df["row"] /= int(multilook[0]) + df["col"] /= int(multilook[2]) + + # ~ print('Saving real and imaginary bands to intermediate GeoTiff outputs') + gs.message( + _("Saving real and imaginary bands to intermediate GeoTiff outputs") + ) + save_bands(bands, basename) + + # ~ print('Reading real and imaginary bands into gs.GIS, and cleaning intermediate files') + gs.message( + _( + "Reading real and imaginary bands into gs.GIS, and cleaning intermediate files" + ) + ) + for band in bands: + input_r = f"{basename}_{band}_real.tif" + input_i = f"{basename}_{band}_imag.tif" + gs.run_command( + "r.import", input=input_r, output=input_r.split(".tif")[0] + ) + gs.run_command( + "r.import", input=input_i, output=input_i.split(".tif")[0] + ) + os.remove(input_r) + os.remove(input_i) + + # ~ print('Saving GCP information as support file') + gs.message(_("Saving GCP information as support file")) + env = gs.gisenv() + gcp_base_folder = os.path.join( + env["GISDBASE"], env["LOCATION_NAME"], env["MAPSET"], "cell_misc", basename + ) + if not os.path.isdir(gcp_base_folder): + os.makedirs(gcp_base_folder) + df.to_csv(os.path.join(gcp_base_folder, "GCPS.csv")) + + +if __name__ == "__main__": + options, flags = gs.parser() + main() diff --git a/src/imagery/i.saocom/i.saocom.import/saocomImportWF.png b/src/imagery/i.saocom/i.saocom.import/saocomImportWF.png new file mode 100644 index 0000000000..5ddac61f50 Binary files /dev/null and b/src/imagery/i.saocom/i.saocom.import/saocomImportWF.png differ diff --git a/src/imagery/i.sar/Makefile b/src/imagery/i.sar/Makefile new file mode 100644 index 0000000000..40646ab490 --- /dev/null +++ b/src/imagery/i.sar/Makefile @@ -0,0 +1,14 @@ +MODULE_TOPDIR =../.. + +PGM = i.sar + +SUBDIRS = i.sar.amplitude \ + i.sar.paulirgb \ + i.sar.speckle \ + +include $(MODULE_TOPDIR)/include/Make/Dir.make + +default: parsubdirs htmldir + +install: installsubdirs + $(INSTALL_DATA) $(PGM).html $(INST_DIR)/docs/html/ diff --git a/src/imagery/i.sar/i.sar.amplitude/Makefile b/src/imagery/i.sar/i.sar.amplitude/Makefile new file mode 100755 index 0000000000..115b542457 --- /dev/null +++ b/src/imagery/i.sar/i.sar.amplitude/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = i.sar.amplitude + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/imagery/i.sar/i.sar.amplitude/i.sar.amplitude.html b/src/imagery/i.sar/i.sar.amplitude/i.sar.amplitude.html new file mode 100755 index 0000000000..4fb1393a55 --- /dev/null +++ b/src/imagery/i.sar/i.sar.amplitude/i.sar.amplitude.html @@ -0,0 +1,24 @@ +

DESCRIPTION

+ +The i.sar.amplitude module is used to calculate an amplitude image from a real and imaginary band of an SLC SAR image. It is intended for any SAR image whose real and +imaginary bands have been imported to GRASS. The module uses r.mapcalc to compute the output. + +

EXAMPLES

+ +

Calculate the amplitude map from SAOCOM-1 imaginary and real bands

+ +
+i.sar.amplitude real=SAO1B_20211222_hh_real imag=SAO1B_20211222_hh_imag output=SAO1B_20211222_hh_amplitude
+
+ +

SEE ALSO

+ +r.mapcalc, +i.saocom.import, +i.saocom.geocode, +i.sar.speckle, +i.sar.pauliRGB + +

AUTHORS

+ +Santiago Seppi, CONAE, Argentina diff --git a/src/imagery/i.sar/i.sar.amplitude/i.sar.amplitude.py b/src/imagery/i.sar/i.sar.amplitude/i.sar.amplitude.py new file mode 100755 index 0000000000..feb2814149 --- /dev/null +++ b/src/imagery/i.sar/i.sar.amplitude/i.sar.amplitude.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: i.sar.amplitude +# +# AUTHOR: Santiago Seppi +# +# PURPOSE: Computation of amplitude map for SLC complex SAR image +# +# COPYRIGHT: (C) 2002-2023 by the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +# +############################################################################# + +# %module +# % description: Calculates amplitude for SAR image +# %end +# %option G_OPT_R_INPUT +# % key: real +# % description: Name of real band +# %end +# %option G_OPT_R_INPUT +# % key: imag +# % description: Name of imaginary band +# %end +# %option G_OPT_R_OUTPUT +# % key: output +# %end + +import grass.script as gs + + +def main(): + options, flags = gs.parser() + real = options["real"] + imag = options["imag"] + raster_output = options["output"] + amplitude_formula = f"{raster_output} = sqrt(pow({real},2) + pow({imag},2))" + + gs.mapcalc(exp=amplitude_formula) + + +if __name__ == "__main__": + main() diff --git a/src/imagery/i.sar/i.sar.html b/src/imagery/i.sar/i.sar.html new file mode 100644 index 0000000000..dbcdb21de3 --- /dev/null +++ b/src/imagery/i.sar/i.sar.html @@ -0,0 +1,44 @@ + + + +i.sar toolset - GRASS GIS manual + + + + + +
+ +GRASS logo +
+ +

NAME

+ +i.sar - Toolset to process SAR products. + +

KEYWORDS

+imagery, +import, +satellite + + +

DESCRIPTION

+ +The i.sar toolset consists of three modules: + +
+
i.sar.speckle
+
A simple procedure to reduce speckle noise in SAR images
+
i.sar.amplitude
+
Module used to calculate an amplitude image from real and imaginary bands
+
i.sar.paulirgb
+
Module used to generate the three bands that form the Pauli RGB SAR composition
+
+ + +

AUTHORS

+Margherita Di Leo
+Santiago Seppi, CONAE, Argentina + +

SOURCE CODE

+ diff --git a/src/imagery/i.sar/i.sar.paulirgb/Makefile b/src/imagery/i.sar/i.sar.paulirgb/Makefile new file mode 100644 index 0000000000..49c2b71d7c --- /dev/null +++ b/src/imagery/i.sar/i.sar.paulirgb/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = i.sar.paulirgb + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/imagery/i.sar/i.sar.paulirgb/i.sar.paulirgb.html b/src/imagery/i.sar/i.sar.paulirgb/i.sar.paulirgb.html new file mode 100644 index 0000000000..38db107e1d --- /dev/null +++ b/src/imagery/i.sar/i.sar.paulirgb/i.sar.paulirgb.html @@ -0,0 +1,43 @@ +

DESCRIPTION

+ +The i.sar.pauliRGB module is used to generate the three bands that form the Pauli RGB SAR composition. The input are amplitude images and the output are three bands: + + + +The output bands are prefixed with the value given to the basename option. Color enhancement can be optionally applied with the strength option, the parameter used +in i.colors.enhance. + +

EXAMPLES

+ +

Create the Pauli RGB bands and apply a color enhancement with a strength of 98%

+ +
+i.sar.pauliRGB hh=SAO1B_20211222_hh_amp_mean7x7 vv=SAO1B_20211222_vv_amp_mean7x7 hv=SAO1B_20211222_hv_amp_mean7x7 basename=SAO1B_20211222 strength=98
+
+#Check the creation of the Pauli bands:
+g.list type=raster pattern='*Pauli*'
+
+#Output:
+SAO1B_20211222_Pauli_Blue
+SAO1B_20211222_Pauli_Green
+SAO1B_20211222_Pauli_Red
+
+ +

REFERENCES

+ +GAMMA Remote Sensing AG. User's Guide: Polarimetric Tools. + +

SEE ALSO

+ +r.mapcalc, +i.colors.enhance, +i.sar.speckle, +i.sar.amplitude + +

AUTHOR

+ +Santiago Seppi, CONAE, Argentina diff --git a/src/imagery/i.sar/i.sar.paulirgb/i.sar.paulirgb.py b/src/imagery/i.sar/i.sar.paulirgb/i.sar.paulirgb.py new file mode 100644 index 0000000000..cfe1505433 --- /dev/null +++ b/src/imagery/i.sar/i.sar.paulirgb/i.sar.paulirgb.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: i.sar.paulirgb +# +# AUTHOR: Santiago Seppi +# +# PURPOSE: Create the RGB bands for the Pauli combination +# +# COPYRIGHT: (C) 2002-2023 by the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +# +############################################################################# + +# %Module +# % description: Create the RGB bands for the Pauli combination. +# % keyword: imagery +# % keyword: sar +# % keyword: radar +# % keyword: rgb +# % overwrite: yes +# %End +# %option G_OPT_R_INPUT +# % key: hh +# % description: HH polarization band +# %end +# %option G_OPT_R_INPUT +# % key: vv +# % description: VV polarization band +# %end +# %option G_OPT_R_INPUT +# % key: hv +# % description: HV polarization band +# %end +# %option G_OPT_R_OUTPUT +# % key: basename +# % description: Prefix for output raster maps +# % required: yes +# %end +# %option +# % key: strength +# % type: double +# % description: Cropping intensity (upper brightness level) +# % options: 0-100 +# % required: no +# %end + +import grass.script as gs + + +def main(): + hh = options["hh"] + vv = options["vv"] + hv = options["hv"] + basename = options["basename"] + strength = options["strength"] + pauli_red = f"{basename}_Pauli_Red = {hh}-{vv}" + gs.mapcalc(exp=pauli_red) + pauli_green = f"{basename}_Pauli_Green = 2*{hv}" + gs.mapcalc(exp=pauli_green) + pauli_blue = f"{basename}_Pauli_Blue = {hh}+{vv}" + gs.mapcalc(exp=pauli_blue) + + if strength: + gs.run_command( + "i.colors.enhance", + red=f"{basename}_Pauli_Red", + green=f"{basename}_Pauli_Green", + blue=f"{basename}_Pauli_Blue", + strength=strength, + ) + + +if __name__ == "__main__": + options, flags = gs.parser() + main() diff --git a/src/imagery/i.sar/i.sar.speckle/Makefile b/src/imagery/i.sar/i.sar.speckle/Makefile new file mode 100644 index 0000000000..614ff88031 --- /dev/null +++ b/src/imagery/i.sar/i.sar.speckle/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = i.sar.speckle + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/imagery/i.sar/i.sar.speckle/i.sar.speckle.html b/src/imagery/i.sar/i.sar.speckle/i.sar.speckle.html new file mode 100644 index 0000000000..e85a890995 --- /dev/null +++ b/src/imagery/i.sar/i.sar.speckle/i.sar.speckle.html @@ -0,0 +1,36 @@ +

DESCRIPTION

+ +

A simple procedure to reduce speckle noise in SAR images. + +

So far, the implemented algorithms are Lee filter, mean and gaussian. The +method parameter is set to lee by default. + +

The size parameter is the one used in +r.neighbors and is used to calculate local +mean and local square mean for the Lee filter. It must be odd. +The same parameter is used for the other two methods, and in case the gaussian one is selected, a standard deviation +value must also be specified. + +

EXAMPLES

+ +Apply a speckle filter using the gaussian method with a 7x7 window and a standard deviation = 1: + +
+i.sar.speckle input=SAO1B_20211222_hh_amp output=SAO1B_20211222_hh_amp_gauss7x7 method=gauss size=7 std=1
+
+ +

REFERENCES

+ +Lee, J. S. (1986). Speckle suppression and analysis for synthetic aperture +radar images. Optical engineering, 25(5), 255636. + +

SEE ALSO

+ +r.neighbors, +i.sar.amplitude, +i.sar.pauliRGB + +

AUTHOR

+ +Margherita Di Leo
+Santiago Seppi, CONAE, Argentina diff --git a/src/imagery/i.sar/i.sar.speckle/i.sar.speckle.py b/src/imagery/i.sar/i.sar.speckle/i.sar.speckle.py new file mode 100755 index 0000000000..13a5e1bf8b --- /dev/null +++ b/src/imagery/i.sar/i.sar.speckle/i.sar.speckle.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: i.sar.speckle +# +# AUTHOR: Margherita Di Leo, Santiago Seppi +# +# PURPOSE: Speckle noise removal for synthetic aperture radar (SAR) images +# +# COPYRIGHT: (C) 2002-2023 by the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +# REFERENCES: +# +# Lee, J. S. (1986). Speckle suppression and analysis for synthetic aperture +# radar images. Optical engineering, 25(5), 255636. +# +############################################################################# + +# %Module +# % description: Remove speckle from SAR image +# % keyword: imagery +# % keyword: speckle +# % keyword: sar +# % keyword: radar +# % overwrite: yes +# %End +# %option G_OPT_R_INPUT +# % key: input +# % description: Name of input image +# % required: yes +# %end +# %option G_OPT_R_OUTPUT +# % key: output +# % description: Name of output image +# % required: yes +# %end +# %option +# % key: method +# % description: Method for speckle removal +# % options: lee,mean,gauss +# % answer: lee +# % required: yes +# %end +# %option +# % key: size +# % type: integer +# % description: Size of neighborhood +# % answer: 11 +# % required: yes +# %end +# %option +# % key: std +# % type: double +# % description: Standard deviation value in case gaussian filter is selected +# % answer: 1 +# % required: no +# %end + + +#import os +import grass.script as gs +import atexit +#from grass.pygrass.modules.shortcuts import general as g +from grass.pygrass.modules.shortcuts import raster as r + +def remove(name): + gs.run_command("g.remove", type="raster", name=name, flags="f", quiet=True, errors="ignore") + +def lee_filter(img, size, img_out): + #pid = str(os.getpid()) + # ~ img_mean = "tmp%s_img_mean" % pid + # ~ img_sqr = "tmp%s_img_sqr" % pid + # ~ img_sqr_mean = "tmp%s_img_sqr_mean" % pid + # ~ img_variance = "tmp%s_img_variance" % pid + # ~ img_weights = "tmp%s_img_weights" % pid + + img_mean = gs.append_node_pid("img_mean") + img_sqr = gs.append_node_pid("img_sqr") + img_sqr_mean = gs.append_node_pid("img_sqr_mean") + img_variance = gs.append_node_pid("img_variance") + img_weights = gs.append_node_pid("img_weights") + atexit.register(remove, img_mean) + atexit.register(remove, img_sqr) + atexit.register(remove, img_sqr_mean) + atexit.register(remove, img_variance) + atexit.register(remove, img_weights) + + + # Local mean + r.neighbors(input=img, size=size, method="average", output=img_mean) + # Local square mean + r.mapcalc("%s = %s^2" % (img_sqr, img)) + r.neighbors(input=img_sqr, size=size, method="average", output=img_sqr_mean) + # Local variance + r.mapcalc("%s = %s - (%s^2)" % (img_variance, img_sqr_mean, img_mean)) + # Overall variance + univar_stats = gs.parse_command("r.univar", map=img, flags="ge") + overall_variance = univar_stats["variance"] + # Weights + r.mapcalc( + "%s = %s / (%s + %s)" + % (img_weights, img_variance, img_variance, overall_variance) + ) + # Output + r.mapcalc( + "%s = %s + %s * (%s - %s)" % (img_out, img_mean, img_weights, img, img_mean) + ) + + # ~ # Cleanup + # ~ gs.message(_("Cleaning up intermediate files...")) + # ~ try: + # ~ gs.run_command( + # ~ "g.remove", flags="f", quiet=False, type="raster", pattern="tmp*" + # ~ ) + # ~ except: + # ~ """ """ + + return img_out + + +def mean_filter(img, size, img_out): + # Local mean + r.neighbors(input=img, size=size, method="average", output=img_out) + + return img_out + + +def gauss_filter(img, size, std, img_out): + # Gauss function + r.neighbors( + input=img, + size=size, + weighting_function="gaussian", + weighting_factor=std, + output=img_out, + ) + + return img_out + + +def main(): + method = options["method"] # algorithm for speckle removal + img = options["input"] # name of input image + img_out = options["output"] # name of output image + size = options["size"] # size of neighborhood + + out = gs.core.find_file(img_out) + + if (out["name"] != "") and not gs.overwrite(): + gs.warning( + _("Output map name already exists. " "Delete it or use overwrite flag") + ) + + if method == "lee": + #g.message(_("Applying Lee Filter")) + img_out = lee_filter(img, size, img_out) + #g.message(_("Done.")) + + elif method == "mean": + #g.message(_("Applying Mean Filter")) + img_out = mean_filter(img, size, img_out) + #g.message(_("Done.")) + + elif method == "gauss": + std = options["std"] + #g.message(_("Applying Gauss Filter")) + img_out = gauss_filter(img, size, std, img_out) + #g.message(_("Done.")) + + else: + gs.fatal(_("The requested speckle filter is not yet implemented.")) + + +if __name__ == "__main__": + options, flags = gs.parser() + main()