¿Encuentra la extensión límite mínima del valor de píxel dado dentro de la trama?

9

Me pregunto si hay una manera de encontrar la extensión de límite mínima para un ráster con un valor particular. Recorté un ráster de una imagen global y la extensión se establece como la extensión global con una gran cantidad de área NoData. Me gustaría eliminar el área NoData de este ráster y retener solo el área que contiene los píxeles del valor particular. ¿Cómo puedo hacer esto?

Aquí está mi ejemplo: me gustaría extraer el valor = 1 (área azul) y usar la extensión del área azul en lugar del mundo entero para su posterior procesamiento.

Imagen de muestra

Visto
fuente
¿Podría publicar una muestra?
Aaron
"Me gustaría eliminar las filas y columnas nulas para este ráster". ¿Qué significa esto exactamente? No entiendo cuál es el resultado final deseado.
blah238
Por "extensión de límite mínima", ¿está buscando una extensión rectangular o una huella poligonal que represente el área de la imagen con datos?
blah238
1
@Tomek, el OP está buscando encontrar el alcance, no tiene que crearlo manualmente.
blah238
1
Si literalmente algo es un juego justo, entonces algún software tiene comandos incorporados para hacer esto; ver reference.wolfram.com/mathematica/ref/ImageCrop.html por ejemplo.
whuber

Respuestas:

6

SI he entendido la pregunta correctamente, parece que quiere saber el cuadro delimitador mínimo de los valores que no son nulos. Tal vez pueda convertir el ráster en polígonos, seleccione los polígonos que le interesan y luego vuelva a convertirlos en un ráster. Luego puede ver los valores de las propiedades que deberían darle el cuadro de límite mínimo.

dango
fuente
1
En total, este es probablemente el mejor enfoque dados los límites del flujo de trabajo de procesamiento de ráster ArcGIS.
blah238
Hice esto exactamente. ¿Hay alguna forma automática? Creo que el algoritmo de ráster a polígono tiene un paso para extraer el cuadro de límite mínimo del ráster.
Visto el
¿Estás buscando una solución de Python?
dango
8

El truco es calcular los límites de los datos que tienen valores. Quizás la forma más rápida, más natural y más general de obtenerlos es con resúmenes zonales: al usar todas las celdas que no son NoData para la zona, el mínimo zonal y el máximo de las cuadrículas que contienen las coordenadas X e Y proporcionarán la extensión completa.

ESRI sigue cambiando las formas en que se pueden hacer estos cálculos; por ejemplo, las expresiones incorporadas para las cuadrículas de coordenadas se eliminaron con ArcGIS 8 y parece que no han regresado. Solo por diversión, aquí hay un conjunto de cálculos rápidos y simples que harán el trabajo sin importar qué.

  1. Convierta la cuadrícula en una sola zona al igualarla consigo misma, como en

    "Mi cuadrícula" == "Mi cuadrícula"

  2. Cree una cuadrícula de índice de columna mediante la acumulación de flujo de una cuadrícula constante con valor 1. (Los índices comenzarán con 0.) Si lo desea, multiplíquelo por el tamaño de celda y agregue la coordenada x del origen para obtener una cuadrícula de coordenadas x " X "(se muestra a continuación).

  3. Del mismo modo, cree una cuadrícula de índice de fila ( y luego una cuadrícula de coordenadas y "Y") mediante la acumulación de flujo de una cuadrícula constante con valor 64.

  4. Use la cuadrícula de zona del paso (1) a calcular el mínimo zonal y el máximo de "X" e "Y" : ahora tiene la extensión deseada.

Imagen final

(La extensión, como se muestra en las dos tablas de estadísticas zonales, se representa con un contorno rectangular en esta figura. La cuadrícula "I" es la cuadrícula de zona obtenida en el paso (1)).

Para ir más allá, necesitará extraer estos cuatro números de sus tablas de salida y usarlos para limitar la extensión del análisis. La copia de la cuadrícula original, con la extensión de análisis restringida en su lugar, completa la tarea.

whuber
fuente
8

Aquí hay una versión del método @whubers para ArcGIS 10.1+ como una caja de herramientas de Python (.pyt).

import arcpy

class Toolbox(object):
    def __init__(self):
        """Define the toolbox (the name of the toolbox is the name of the
        .pyt file)."""
        self.label = "Raster Toolbox"
        self.alias = ""

        # List of tool classes associated with this toolbox
        self.tools = [ClipNoData]


class ClipNoData(object):
    def __init__(self):
        """Clip raster extent to the data that have values"""
        self.label = "Clip NoData"
        self.description = "Clip raster extent to the data that have values. "
        self.description += "Method by Bill Huber - https://gis.stackexchange.com/a/55150/2856"

        self.canRunInBackground = False

    def getParameterInfo(self):
        """Define parameter definitions"""
        params = []

        # First parameter
        params+=[arcpy.Parameter(
            displayName="Input Raster",
            name="in_raster",
            datatype='GPRasterLayer',
            parameterType="Required",
            direction="Input")
        ]

        # Second parameter
        params+=[arcpy.Parameter(
            displayName="Output Raster",
            name="out_raster",
            datatype="DERasterDataset",
            parameterType="Required",
            direction="Output")
        ]

        return params

    def isLicensed(self):
        """Set whether tool is licensed to execute."""
        return arcpy.CheckExtension('spatial')==u'Available'

    def execute(self, parameters, messages):
        """See https://gis.stackexchange.com/a/55150/2856
           ##Code comments paraphrased from @whubers GIS StackExchange answer
        """
        try:
            #Setup
            arcpy.CheckOutExtension('spatial')
            from arcpy.sa import *
            in_raster = parameters[0].valueAsText
            out_raster = parameters[1].valueAsText

            dsc=arcpy.Describe(in_raster)
            xmin=dsc.extent.XMin
            ymin=dsc.extent.YMin
            mx=dsc.meanCellWidth
            my=dsc.meanCellHeight
            arcpy.env.extent=in_raster
            arcpy.env.cellSize=in_raster
            arcpy.AddMessage(out_raster)

            ## 1. Convert the grid into a single zone by equating it with itself
            arcpy.AddMessage(r'1. Convert the grid into a single zone by equating it with itself...')
            zones = Raster(in_raster) == Raster(in_raster)

            ## 2. Create a column index grid by flow-accumulating a constant grid
            ##    with value 1. (The indexes will start with 0.) Multiply this by
            ##    the cellsize and add the x-coordinate of the origin to obtain
            ##    an x-coordinate grid.
            arcpy.AddMessage(r'Create a constant grid...')
            const = CreateConstantRaster(1)

            arcpy.AddMessage(r'2. Create an x-coordinate grid...')
            xmap = (FlowAccumulation(const)) * mx + xmin

            ## 3. Similarly, create a y-coordinate grid by flow-accumulating a
            ##    constant grid with value 64.
            arcpy.AddMessage(r'3. Create a y-coordinate grid...')
            ymap = (FlowAccumulation(const * 64)) * my + ymin

            ## 4. Use the zone grid from step (1) to compute the zonal min and
            ##    max of "X" and "Y"
            arcpy.AddMessage(r'4. Use the zone grid from step (1) to compute the zonal min and max of "X" and "Y"...')

            xminmax=ZonalStatisticsAsTable(zones, "value", xmap,r"IN_MEMORY\xrange", "NODATA", "MIN_MAX")
            xmin,xmax=arcpy.da.SearchCursor(r"IN_MEMORY\xrange", ["MIN","MAX"]).next()

            yminmax=ZonalStatisticsAsTable(zones, "value", ymap,r"IN_MEMORY\yrange", "NODATA", "MIN_MAX")
            ymin,ymax=arcpy.da.SearchCursor(r"IN_MEMORY\yrange", ["MIN","MAX"]).next()

            arcpy.Delete_management(r"IN_MEMORY\xrange")
            arcpy.Delete_management(r"IN_MEMORY\yrange")

            # Write out the reduced raster
            arcpy.env.extent = arcpy.Extent(xmin,ymin,xmax+mx,ymax+my)
            output = Raster(in_raster) * 1
            output.save(out_raster)

        except:raise
        finally:arcpy.CheckInExtension('spatial')
usuario2856
fuente
Muy lindo Luke. Autónomo, ejecutable, utiliza in_memory y está bien comentado para arrancar. Tuve que desactivar el procesamiento en segundo plano ( Geoprocesamiento> opciones> ... ) para que funcione.
Matt Wilkie
1
Actualicé el script y configuré canRunInBackground = False. Notaré que vale la pena cambiar los entornos de espacio de trabajo / scratchworkspace a una carpeta local (no FGDB) como descubrí cuando los dejé como predeterminados (es decir, <perfil de usuario de red> \ Default.gdb) ¡¡el script tardó 9 minutos !!! para ejecutarse en una cuadrícula de celdas de 250x250. Cambiando a un FGDB local tomó 9 segundos y a una carpeta local 1-2 segundos ...
user2856
Buen punto sobre las carpetas locales, y gracias por la rápida solución de fondo (mucho mejor que escribir instrucciones para todos los que se lo paso). Podría valer la pena lanzar esto en bitbucket / github / gcode / etc.
Matt Wilkie
+1 ¡Gracias por esta contribución, Luke! Le agradezco que haya llenado el espacio (bastante grande) que queda en mi respuesta.
whuber
4

He ideado una solución basada en gdal y numpy. Rompe la matriz ráster en filas y columnas y suelta cualquier fila / columna vacía. En esta implementación, "vacío" es cualquier cosa menor que 1, y solo se tienen en cuenta los rásteres de banda única.

(Me doy cuenta mientras escribo que este enfoque de línea de exploración solo es adecuado para imágenes con "collares" de nodata. Si sus datos son islas en mares de nulos, el espacio entre islas también se eliminará, aplastando todo y arruinando totalmente la georreferenciación .)

Las partes del negocio (necesita desarrollarse, no funcionará como está):

    #read raster into a numpy array
    data = np.array(gdal.Open(src_raster).ReadAsArray())
    #scan for data
    non_empty_columns = np.where(data.max(axis=0)>0)[0]
    non_empty_rows = np.where(data.max(axis=1)>0)[0]
        # assumes data is any value greater than zero
    crop_box = (min(non_empty_rows), max(non_empty_rows),
        min(non_empty_columns), max(non_empty_columns))

    # retrieve source geo reference info
    georef = raster.GetGeoTransform()
    xmin, ymax = georef[0], georef[3]
    xcell, ycell = georef[1], georef[5]

    # Calculate cropped geo referencing
    new_xmin = xmin + (xcell * crop_box[0]) + xcell
    new_ymax = ymax + (ycell * crop_box[2]) - ycell
    cropped_transform = new_xmin, xcell, 0.0, new_ymax, 0.0, ycell

    # crop
    new_data = data[crop_box[0]:crop_box[1]+1, crop_box[2]:crop_box[3]+1]

    # write to disk
    band = out_raster.GetRasterBand(1)
    band.WriteArray(new_data)
    band.FlushCache()
    out_raster = None

En un guión completo:

import os
import sys
import numpy as np
from osgeo import gdal

if len(sys.argv) < 2:
    print '\n{} [infile] [outfile]'.format(os.path.basename(sys.argv[0]))
    sys.exit(1)

src_raster = sys.argv[1]
out_raster = sys.argv[2]

def main(src_raster):
    raster = gdal.Open(src_raster)

    # Read georeferencing, oriented from top-left
    # ref:GDAL Tutorial, Getting Dataset Information
    georef = raster.GetGeoTransform()
    print '\nSource raster (geo units):'
    xmin, ymax = georef[0], georef[3]
    xcell, ycell = georef[1], georef[5]
    cols, rows = raster.RasterYSize, raster.RasterXSize
    print '  Origin (top left): {:10}, {:10}'.format(xmin, ymax)
    print '  Pixel size (x,-y): {:10}, {:10}'.format(xcell, ycell)
    print '  Columns, rows    : {:10}, {:10}'.format(cols, rows)

    # Transfer to numpy and scan for data
    # oriented from bottom-left
    data = np.array(raster.ReadAsArray())
    non_empty_columns = np.where(data.max(axis=0)>0)[0]
    non_empty_rows = np.where(data.max(axis=1)>0)[0]
    crop_box = (min(non_empty_rows), max(non_empty_rows),
        min(non_empty_columns), max(non_empty_columns))

    # Calculate cropped geo referencing
    new_xmin = xmin + (xcell * crop_box[0]) + xcell
    new_ymax = ymax + (ycell * crop_box[2]) - ycell
    cropped_transform = new_xmin, xcell, 0.0, new_ymax, 0.0, ycell

    # crop
    new_data = data[crop_box[0]:crop_box[1]+1, crop_box[2]:crop_box[3]+1]

    new_rows, new_cols = new_data.shape # note: inverted relative to geo units
    #print cropped_transform

    print '\nCrop box (pixel units):', crop_box
    print '  Stripped columns : {:10}'.format(cols - new_cols)
    print '  Stripped rows    : {:10}'.format(rows - new_rows)

    print '\nCropped raster (geo units):'
    print '  Origin (top left): {:10}, {:10}'.format(new_xmin, new_ymax)
    print '  Columns, rows    : {:10}, {:10}'.format(new_cols, new_rows)

    raster = None
    return new_data, cropped_transform


def write_raster(template, array, transform, filename):
    '''Create a new raster from an array.

        template = raster dataset to copy projection info from
        array = numpy array of a raster
        transform = geo referencing (x,y origin and pixel dimensions)
        filename = path to output image (will be overwritten)
    '''
    template = gdal.Open(template)
    driver = template.GetDriver()
    rows,cols = array.shape
    out_raster = driver.Create(filename, cols, rows, gdal.GDT_Byte)
    out_raster.SetGeoTransform(transform)
    out_raster.SetProjection(template.GetProjection())
    band = out_raster.GetRasterBand(1)
    band.WriteArray(array)
    band.FlushCache()
    out_raster = None
    template = None

if __name__ == '__main__':
    cropped_raster, cropped_transform = main(src_raster)
    write_raster(src_raster, cropped_raster, cropped_transform, out_raster)

El script está en mi código oculto en Github, si el enlace va 404 cazar un poco; Estas carpetas están maduras para alguna reorganización.

wilkie mate
fuente
1
Esto no funcionará para conjuntos de datos realmente grandes. Consigo unMemoryError Source raster (geo units): Origin (top left): 2519950.0001220703, 5520150.00012207 Pixel size (x,-y): 100.0, -100.0 Columns, rows : 42000, 43200 Traceback (most recent call last): File "D:/11202067_COACCH/local_checkout/crop_raster.py", line 72, in <module> cropped_raster, cropped_transform = main(src_raster) File "D:/11202067_COACCH/local_checkout/crop_raster.py", line 22, in main data = np.array(raster.ReadAsArray()) MemoryError
user32882
2

A pesar de todo su poder analítico, ArcGIS carece de manipulaciones ráster básicas que puede encontrar con editores de imágenes de escritorio tradicionales como GIMP . Espera que desee utilizar la misma extensión de análisis para su ráster de salida que su ráster de entrada, a menos que anule manualmente la configuración del entorno de Extensión de salida . Dado que esto es exactamente lo que está buscando encontrar, no establecer, la forma de hacer las cosas de ArcGIS se interpone en el camino.

A pesar de mis mejores esfuerzos, y sin recurrir a la programación, no pude encontrar la manera de obtener el alcance de su subconjunto deseado de la imagen (sin la conversión de trama a vector que es un desperdicio computacional).

En cambio, usé GIMP para seleccionar el área azul usando la herramienta Seleccionar por color y luego invertí la selección, presioné Eliminar para eliminar el resto de los píxeles, volví a invertir la selección, recorté la imagen a la selección y finalmente la volví a exportar a PNG. GIMP lo guardó como una imagen de profundidad de 1 bit. El resultado está abajo:

Salida

Por supuesto, debido a que su imagen de muestra carecía de un componente de referencia espacial, y GIMP no es consciente del espacio, la imagen de salida es casi tan útil como su entrada de muestra. Deberá georreferenciarlo para que sea útil en un análisis espacial.

blah238
fuente
En realidad, esta operación solía ser fácil en versiones anteriores de Spatial Analyst: el zonal máximo y mínimo de las dos cuadrículas de coordenadas (X e Y), usando la función como la zona, dan la extensión exactamente. (Bueno, es posible que desee expandirlo a la mitad de un tamaño de celda en las cuatro direcciones). En ArcGIS 10, debe ser creativo (o usar Python) para hacer una cuadrícula de coordenadas. En cualquier caso, todo se puede hacer completamente dentro de SA utilizando solo operaciones de red y sin intervención manual.
whuber
@whuber Vi su solución en otro lugar, pero aún no estoy seguro de cómo puedo implementar su método. ¿Podrías mostrarme más detalles de esto?
Visto el
@Seen Una búsqueda rápida de este sitio encuentra una cuenta del método en gis.stackexchange.com/a/13467 .
whuber
1

Aquí hay una posibilidad usando SAGA GIS: http://permalink.gmane.org/gmane.comp.gis.gdal.devel/33021

En SAGA GIS hay un módulo "Recortar a datos" (en la biblioteca de módulos de Grid Tools), que realiza la tarea.

Pero esto requeriría que importe su Geotif con el módulo de importación GDAL, lo procese en SAGA y finalmente lo exporte como Geotif nuevamente con el módulo de exportación GDAL.

Otra posibilidad usando solo las herramientas de ArcGIS GP sería construir un TIN a partir de su ráster usando Raster to TIN , calcular su límite usando el dominio TIN y recortar su ráster por el límite (o su envoltura usando Feature Envelope to Polygon ).

blah238
fuente