在栅格中找到给定像素值的最小边界范围?


9

我想知道是否有办法找到具有特定值的栅格的最小边界范围。我从全局图像中裁剪了一个栅格,并将范围设置为具有大量NoData区域的全局范围。我想从此栅格中删除NoData区域,并仅保留包含特定值像素的区域。我怎样才能做到这一点?

这是我的示例:我想提取value = 1(蓝色区域)并使用蓝色区域的范围而不是整个世界进行进一步处理。

样本图片


你能发个样品吗?
亚伦

“我想删除此栅格的空行和列。” 这到底是什么意思?我不明白预期的最终结果是什么。
blah238

通过“最小边界范围”,您是在寻找一个矩形范围或一个多边形足迹,该范围或多边形足迹代表具有数据的图像区域吗?
blah238 2013年

1
@Tomek中,OP是希望找到的范围内,不必手动创建它。
blah238 2013年

1
如果从字面上看,任何事情都是公平的游戏,那么某些软件具有内置命令可以执行此操作。例如,请参见reference.wolfram.com/mathematica/ref/ImageCrop.html
ub

Answers:


6

如果我已经正确理解了这个问题,这听起来像是您想知道不为null的值的最小边界框。也许您可以将栅格转换为多边形,选择感兴趣的多边形,然后将其转换回栅格。然后,您可以查看应该为您提供最小边框的属性值。


1
综上所述,鉴于ArcGIS栅格处理工作流程的局限性,这可能是最好的方法。
blah238 2013年

我正是这样做的。有没有自动的方法?我认为栅格转多边形算法有一个步骤可以提取栅格的最小边界框。
看到

您在寻找python解决方案吗?
dango 2013年

8

诀窍是计算具有值的数据的限制。 也许最快,最自然,最通用的方法是使用区域汇总:通过将所有非NoData单元用于区域,包含X和Y坐标的网格的区域最小值和最大值将提供完整范围。

ESRI不断改变进行这些计算的方式。例如,坐标网格的内置表达式已随ArcGIS 8一起删除,并且似乎未返回。只是为了好玩,这是一组快速,简单的计算,无论如何都可以完成工作。

  1. 通过将网格与其自身等价,将网格转换为单个区域,如

    “我的网格” ==“我的网格”

  2. 通过对值1的常数网格进行流累积来创建列索引网格。(索引将从0开始。)如果需要,将其乘以单元格大小并添加原点的x坐标即可获得x坐标网格 “ X”(如下所示)。

  3. 类似地,通过对值64的常数栅格进行流累积来创建行索引栅格(然后创建y坐标栅格 “ Y”)。

  4. 使用步骤(1)中的区域网格来计算“ X”和“ Y”的区域最小值和最大值:现在您具有所需的范围。

最终影像

(该区域的范围,如两个区域统计表中所示,在此图中用矩形轮廓线表示。网格“ I”是在步骤(1)中获得的区域网格。)

要走得更远,您将需要从它们的输出表中提取这四个数字,并使用它们来限制分析范围。复制原始网格,并在受限的分析范围内完成任务。


8

这是ArcGIS 10.1+ 的@whubers方法的一个版本,作为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')

很好,卢克。自包含的,可运行的,使用in_memory,并具有良好的引导注释。我必须关闭后台处理(Geoprocessing> options> ...)才能使其正常工作。
马特·威尔基

1
我已经更新了脚本并设置canRunInBackground = False。我将注意到,有必要将工作区/临时工作区环境更改为本地文件夹(不是FGDB),因为我将脚本保留为默认值(即<网络用户配置文件> \ Default.gdb)时,脚本花费了9分钟!在250x250的单元格上运行。更改为本地FGDB需要9秒钟,更改为本地文件夹1-2秒钟...
2013年

关于本地文件夹的好处,并且感谢快速的后台修复(比为我传递给它的每个人编写说明要好得多)。可能值得在bitbucket / github / gcode / etc上扔掉它。
马特·威尔基

+1感谢卢克的贡献!感谢您填补我的答案中的(较大)空白。
Whuber

4

我设计了一个基于gdal和numpy的解决方案。它将栅格矩阵分为行和列,并删除任何空的行/列。在此实现中,“空”的值小于1,并且仅占单个波段栅格的值。

(当我写这篇文章时,我意识到这种扫描线方法仅适用于没有数据“项圈”的图像。如果您的数据是零星的岛屿,那么岛屿之间的空间也会掉落,将所有东西挤在一起,完全弄乱了地理配准)

业务部分(需要充实,不能按原样工作):

    #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

在完整脚本中:

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)

该脚本位于在Github上的代码存储区中,如果链接稍微跳404;这些文件夹已经成熟,可以进行一些重组。


1
这对于真正的大型数据集将不起作用。我MemoryError 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

尽管具有所有分析能力,但是ArcGIS缺乏基本的栅格操作,而传统的桌面图像编辑器(如GIMP)可以找到这些基本的栅格操作。它期望您对输出栅格使用与输入栅格相同的分析范围,除非您手动覆盖“ 输出范围”环境设置。由于这正是您要查找的东西,而不是设置的东西,因此ArcGIS的处事方式正在妨碍您。

尽管我尽了最大的努力,并且没有求助于编程,但我找不到方法来获得所需图像子集的范围(没有栅格到矢量的转换,这在计算上是浪费的)。

相反,我使用GIMP使用按颜色选择工具来选择蓝色区域,然后反转选择,按Delete键删除其余像素,再次反转选择,将图像裁剪为选择,最后将其导出回PNG。GIMP将其保存为1位深度图像。结果如下:

输出量

当然,由于您的样本图像缺少空间参考成分,并且GIMP没有空间感知,因此输出图像与样本输入一样有用。您将需要对其进行地理配准,以便在空间分析中使用它。


其实,这种操作使用很容易在空间分析以前的版本:两者的纬向最大值和最小值的坐标网格(X和Y),使用该功能的区域,正好给的程度。(好吧,您可能希望在所有四个方向上将其扩大半个像元大小。)在ArcGIS 10中,您需要发挥创造力(或使用Python)制作坐标网格。无论如何,仅通过网格操作可以完全在SA中完成整个过程,而无需人工干预。
whuber

@whuber我在其他地方看到了您的解决方案,但仍然不确定如何实现您的方法。您能告诉我更多细节吗?
2013年

@Seen快速搜索该站点,位于gis.stackexchange.com/a/13467上找到该方法的帐户。
whuber

1

这是使用SAGA GIS的一种可能性:http : //permalink.gmane.org/gmane.comp.gis.gdal.devel/33021

在SAGA GIS中,有一个“裁剪到数据”模块(在Grid Tools模块库中),该模块执行任务。

但这需要您使用GDAL导入模块导入Geotif,在SAGA中对其进行处理,最后再通过GDAL导出模块将其导出为Geotif。

仅使用ArcGIS GP工具的另一种可能性是使用Raster到 TIN从栅格构建TIN,使用TIN Domain计算边界,然后按边界(或使用Feature Envelope转换为Polygon的信封修剪栅格。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.