+
+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
+
+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.
+
+
+
+
+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']:
+
+
+
+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
+
+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.
+
+
+
+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
+
+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:
+
+
+
Band for the red channel, equal to HH-VV
+
Band for the green channel, equal to 2*HV
+
Band for the blue channel, equal to HH+VV
+
+
+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
+
+
+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:
+
+