1

I am trying to merge a variable number of geotiffs into a single, clipped output geotiff, using GDAL 's Python bindings (or via command line).

Requirements:

  • there will be 2 to N input files each time
  • the inputs are all in a common projection, with different (but overlapping) extents
  • the input files contain no_data pixels
  • I want to keep the maximum input value at each each output pixel
  • I want to clip the final image to a known extent
  • the final output will be in the same projection as the input files

Ideally, I'd like to use something like gdal.Warp on a VRT that is built using the input files, to avoid having to create intermediate files/arrays. However, my experience and research indicate that a VRT will use pixels based on the order in which the input files are specified, which is also the case for gdal_merge and gdal_translate.

This seems like it should be a common task, but I've been unable to find a solution that works. The most likely looking approach I've found is to use a PixelFunction, based on this example: Combining overlapping rasters by minimum value in python. Failing so, far though. I have implemented this:

    ########### Create the VRT ###########
    vrt_file = 'mosaic.vrt'
    cmd = ['gdalbuildvrt', vrt_file]
    for file in file_list:
        cmd.append(file) # add all the files to be mosaicked

    proc = subprocess.Popen(cmd)
    stdout,stderr=proc.communicate()
    exit_code=proc.wait()

    if exit_code:
        raise RuntimeError(stderr)
    else:print(stdout)

    ########### Add the Pixel Function ###########
    try_pixel_function = True
    gdal.SetConfigOption('GDAL_VRT_ENABLE_PYTHON', 'YES')

    if try_pixel_function: # FAILS WHEN THIS BLOCK IS RUN
        # Adapted from https://gis.stackexchange.com/questions/350233/how-can-i-modify-a-vrtrasterband-sub-class-etc-from-python
        from lxml import etree
        vrt_tree = etree.parse(vrt_file)
        vrt_root = vrt_tree.getroot()
        vrt_band = vrt_root.findall(".//VRTRasterBand[@band='1']")[0]
        vrt_band.set('subClass', 'VRTDerivedRasterBand')
        pixelFunctionType = etree.SubElement(vrt_band, 'PixelFunctionType')
        pixelFunctionType.text = 'find_max'
        pixelFunctionLanguage = etree.SubElement(vrt_band, 'PixelFunctionLanguage')
        pixelFunctionLanguage.text = 'Python'
        pixelFunctionCode = etree.SubElement(vrt_band, 'PixelFunctionCode')
        pixelFunctionCode.text = etree.CDATA("""
        import numpy as np

        def find_max(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, raster_ysize, buf_radius, gt, **kwargs):
            np.amax(in_ar, axis=0, initial=255, out=out_ar)
        """)
        vrt_tree.write(vrt_file)

    ########### get vrt's projection info ###########
    ds=gdal.Open(vrt_file)
    prj=ds.GetProjection()
    print(prj)

    ########### create mosaic ###########
    outfile_name = 'scratch.tif'
    cmd = ['gdalwarp', '-s_srs', prj, '-t_srs', 'EPSG:3857', '-r', 'max', '-of', 'VRT', vrt_file, outfile_name, '-te_srs', 'EPSG:4326', '-te', str(min_lon), str(min_lat), str(max_lon), str(max_lat), '-ts', str(resolution), str(resolution), '-et', '0', '-ot', 'Int16', '-ovr', 'NONE', '-srcnodata', '0', '-overwrite']

    proc = subprocess.Popen(cmd)
    stdout,stderr=proc.communicate()
    exit_code=proc.wait()

    if exit_code:
        raise RuntimeError(stderr)
    else:print(stdout)

    ########### retrieve the mosaicked data ###########
    ds = gdal.Open(outfile_name)
    data = ds.GetRasterBand(1).ReadAsArray()
    gdal.SetConfigOption('GDAL_VRT_ENABLE_PYTHON', 'NO')

This works fine when try_pixel_function is false (except that the last file overwrites everything else, as mentioned above), but fails on the last line, data = ds.GetRasterBand(1).ReadAsArray(), when try_pixel_function is true, with the error:

ERROR 1: Couldn't compile code: <class 'IndentationError'>, ('unexpected indent', ('gdal_vrt_module_0x287682af0', 6, 8, '
import numpy as np\n', 6, -1)) ERROR 1: scratch.tif, band 1: IReadBlock failed at X offset 0, Y offset 0: Couldn't compile code: <class 'IndentationError'>, ('unexpected indent', ('gdal_vrt_module_0x287682af0', 6, 8, ' import numpy as np\n', 6, -1))

I'm guessing the indent error refers to the code I'm trying to add using pixelFunctionCode.text = etree.CDATA(...

Any suggestions on what I'm doing wrong?

Other research: I also looked at using gdal_calc, as in this example from https://gdal.org/programs/gdal_calc.html

gdal_calc.py -A input1.tif -B input2.tif -C input3.tif --outfile=result.tif --calc="numpy.max((A,B,C),axis=0)

This seems doable but not super performant. I may have up to about 20 input files. I would have to either run on the inputs (which could involve wasting a lot of computation on the overlap areas that will be outside the desired final extent), OR clip each input individually to the final output extent before running this max operation.

(Most relevant results I've found:)

8
  • 1
    This is a discourse and not a question, please read the site rules gis.stackexchange.com/help/how-to-ask then revise your question to be more focused on a single problem you are experiencing. Feb 17 at 2:31
  • For what it's worth, although I haven't tested it, the code in GDAL_Merge.py could be modified to return the maximum of cells: if you copy the script to a new file (eg: GDAL_MergeMax.py) file then change both instances of to_write = Numeric.choose(nodata_test, (data_src, data_dst)) with to_write = Numeric.choose(nodata_test, (Numeric.choose(Numeric.equal(data_dst,nodata),(data_src,Numeric.maximum(data_src, data_dst))), data_dst)) you should be able to overlay to maximum pixel vales of all source rasters, theoretically. Feb 17 at 2:57
  • 1
    Did you use the configuration option GDAL_VRT_ENABLE_PYTHON=YES?
    – user30184
    Feb 17 at 6:31
  • @user30184 - yes, using updated code above, now getting a different error as noted above. I have tried changing the indents in the code I add using pixelFunctionCode.text, but still get the same error.
    – bphi
    Feb 17 at 13:17
  • @MichaelStimson; I've tried to revise - would like to know what is wrong with my current usage of a Pixel Function.
    – bphi
    Feb 17 at 13:20

1 Answer 1

0

I think the error comes in this part:

        pixelFunctionCode.text = etree.CDATA("""
        import numpy as np

        def find_max(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, raster_ysize, buf_radius, gt, **kwargs):
            np.amax(in_ar, axis=0, initial=255, out=out_ar)
        """)

whereas it should be:

        pixelFunctionCode.text = etree.CDATA("""import numpy as np
def find_max(in_ar, out_ar, xoff, yoff, xsize, ysize, raster_xsize, raster_ysize, buf_radius, gt, **kwargs):
    np.amax(in_ar, axis=0, initial=255, out=out_ar)""")

Note: Because of browser I indented with 4 * " " (whitespace). This might lead to problems, so check this if it doesnt work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.