遥感图像处理中常用的python操作

本章节主要参考《python地理空间分析指南》第六章

文章中的所有操作都可以在ENVI中完成,这里只提供一个图像处理的思路。


一、图像波段变换

波段变换最常用的地方就是进行图像显示,例如使用假彩色图像以凸显植被信息。图像波段变换即将图像波段的组合顺序重新排列并显示的方法,主要使用GDAL库。下面以一个例子进行实验,首先给出数据的下载地址:

http://git.io/vqs41

打开原图可以看到,植被明显呈现红色(植被在绿光波段反射率高,而图像将绿光波段放到了红色通道),说明该图为假彩色影像:

在本示例中,将使用gdal_array模块加载图片到numpy数组中,然后将其另存为新的geotiff文件。需要注意的是,numpy引用数组的方式是(y,x)(行,列)型的,常用的电子表格及其他软件则是(x,y)(行,列)型。

下面给出波段变换的代码:

from osgeo import gdal_array

# 源图片的名称
src = "FalseColor.tif"
# 将源图片载入到数组中
arr = gdal_array.LoadFile(src)
# 交换波段1和波段2的位置,使用“高级分片”功能直接对波段进行重新排列
output = gdal_array.SaveArray(arr[[1, 0, 2], :], "swap.tif", format="GTiff",prototype=src)
# 取消输出,避免在某些平台上损坏文件
output = None

波段变换后的影像为:

二、创建直方图

这里创建的直方图的是像元值得频数直方图,横轴代表像元值,纵轴代表像元数量。本示例使用的数据是上述波段变换的结果swap.tif影像。首先给出创建直方图的代码:

from osgeo import gdal_array
import turtle as t


def histogram(a, bins=list(range(0, 256))):
    fa = a.flat
    n = gdal_array.numpy.searchsorted(gdal_array.numpy.sort(fa), bins)
    n = gdal_array.numpy.concatenate([n, [len(fa)]])
    hist = n[1:] - n[:-1]
    return hist


def draw_histogram(hist, scale=True):
    t.color("black")
    axes = ((-355, -200), (355, -200), (-355, -200), (-355, 250))
    t.up()
    for p in axes:
        t.goto(p)
        t.down()
    t.up()
    t.goto(0, -250)
    t.write("VALUE", font=("Arial, ", 12, "bold"))
    t.up()
    t.goto(-400, 280)
    t.write("FREQUENCY", font=("Arial, ", 12, "bold"))
    x = -355
    y = -200
    t.up()
    for i in range(1, 11):
        x = x+65
        t.goto(x, y)
        t.down()
        t.goto(x, y-10)
        t.up()
        t.goto(x, y - 25)
        t.write("{}".format((i*25)), align="center")
    x = -355
    y = -200
    t.up()
    pixels = sum(hist[0])
    if scale:
        max = 0
        for h in hist:
            hmax = h.max()
            if hmax > max:
                max = hmax
        pixels = max
    label = int(pixels/10)
    for i in range(1, 11):
        y = y+45
        t.goto(x, y)
        t.down()
        t.goto(x-10, y)
        t.up()
        t.goto(x-15, y-6)
        t.write("{}".format((i*label)), align="right")
    x_ratio = 709.0 / 256
    y_ratio = 450.0 / pixels
    colors = ["red", "green", "blue"]
    for j in range(len(hist)):
        h = hist[j]
        x = -354
        y = -199
        t.up()
        t.goto(x, y)
        t.down()
        t.color(colors[j])
    for i in range(256):
        x = i * x_ratio
        y = h[i] * y_ratio
        x = x - (709/2)
        y = y + -199
        t.goto((x, y))


img = "swap.tif"
histograms = []
arr = gdal_array.LoadFile(img)
for b in arr:
    histograms.append(histogram(b))
    draw_histogram(histograms)
    t.pen(shown=False)
    t.done

执行上述代码,创建出的直方图为:

为了将上述代码进行绝对比例显示,我们将上述代码的倒数第三行修改为:

draw_histogram(histograms, scale=False)

执行上述代码,显示的结果为:

可以看到,图像的灰度分布明显偏暗,因此我们需要对直方图进行均衡化处理。直方图均衡化处理后的影像的明暗程度更为均匀。下面给出直方图均衡化的代码:

from osgeo import gdal_array
import operator
from functools import reduce


def histogram(a, bins=list(range(0, 256))):
    fa = a.flat
    n = gdal_array.numpy.searchsorted(gdal_array.numpy.sort(fa), bins)
    n = gdal_array.numpy.concatenate([n, [len(fa)]])
    hist = n[1:] - n[:-1]
    return hist


def stretch(a):
    """
    在gdal_array上传的图像数组中执行直方图均衡化操作
    """
    hist = histogram(a)
    lut = []
    for b in range(0, len(hist), 256):
        # 步长尺寸
        step = reduce(operator.add, hist[b:b+256]) / 255
        # 创建均衡的查找表
        n = 0
        for i in range(256):
            lut.append(n/step)
            n = n + hist[i+b]
    gdal_array.numpy.take(lut, a, out=a)
    return a


src = "swap.tif"
arr = gdal_array.LoadFile(src)
stretched = stretch(arr)
output = gdal_array.SaveArray(arr, "stretched.tif", format="GTIFF", prototype=src)
output = None

经过直方图均衡化后的影像结果为:

为了直观的展示直方图均衡化前后的影像变化,这里使用ENVI中的view swipe功能,左边显示的是未均衡化的影像,右边显示的是均衡化后的影像,可以明显看出两幅影像的差异。

重新运行绘制直方图的代码,从结果中可以看到,三个波段的DN值不再明显偏小,而是相对均匀一些。显示的结果为:

三、图像裁剪

我们在处理遥感图像的过程中,往往需要根据已有的shapefile文件来裁剪影像。一般图像裁剪的思路是:加载影像——读取shp文件——栅格化shp文件——将shp做成mask——对影像进行filter——保存裁剪的结果。这里我们主要使用GDAL库进行操作。本示例将会使用县界shapefile文件来裁剪遥感影像,这里的遥感影像是上述创建直方图过程中,进行直方图均衡化后的遥感影像。不过遥感影像的预处理对图像裁剪步骤影像不大。

原始数据的下载地址为:

遥感数据(需要手动进行波段变换和直方图均衡化):http://git.io/vqs41

shapefile数据:http://git.io/vqsRH

在ENVI中打开原始影像和shapefile文件,显示为:

下面利用shapefile文件对遥感影像进行裁剪:

import operator
from osgeo import gdal, gdal_array, osr
import shapefile
from PIL import ImageDraw, Image

# 用于裁剪的栅格影像
raster = "stretched.tif"
# 用于裁剪的多边形shapefile文件
shp = "hancock"
# 裁剪后的栅格文件名
output = "clip"


def imageToArray(i):
    """
    将一个python影像库的数组转换为一个gdal_array图片
    """
    a = gdal_array.numpy.fromstring(i.tostring(), 'b')
    a.shape = i.im.size[1], i.im.size[0]
    return a


def world2pixel(geoMatrix, x, y):
    """
    使用gdal库的geomatrix对象(gdal.GetGeoTransform())计算地理坐标的像素位置
    """
    ulX = geoMatrix[0]
    ulY = geoMatrix[3]
    xDist = geoMatrix[1]
    yDist = geoMatrix[5]
    rtnX = geoMatrix[2]
    rtnY = geoMatrix[4]
    pixel = int((x - ulX) / xDist)
    line = int((ulY - y) / abs(yDist))
    return (pixel, line)


# 将源数据作为gdal_array载入
srcArray = gdal_array.LoadFile(raster)
# 同时载入gdal库的图片从而获取geotransform
srcImage = gdal.Open(raster)
geoTrans = srcImage.GetGeoTransform()
# 使用pyshp库打开shapefile文件
r = shapefile.Reader("{}.shp".format(shp))
# 将图层扩展转换为图片像素坐标
minX, minY, maxX,maxY = r.bbox
ulX, ulY = world2pixel(geoTrans, minX, maxY)
lrX, lrY = world2pixel(geoTrans, maxX, minY)
# 计算新图像的像素尺寸
pxWidth = int(lrX - ulX)
pxHeight = int(lrY - ulY)
clip = srcArray[:, ulY:lrY, ulX:lrX]
# 为图片创建一个新的geomatrix对象以便附加地理参照数据
geoTrans = list(geoTrans)
geoTrans[0] = minX
geoTrans[3] = maxY
# 在一个空白的8字节黑白遮罩图片上把点映射为像素,绘制县市边界线
pixels = []
for p in r.shape(0).points:
    pixels.append(world2pixel(geoTrans, p[0], p[1]))
rasterPoly = Image.new("L", (pxWidth, pxHeight), 1)
# 使用PIL创建一个空白图片用于绘制多边形
rasterize = ImageDraw.Draw(rasterPoly)
rasterize.polygon(pixels, 0)
# 将PIL图片转换为numpy数组
mask = imageToArray(rasterPoly)
# 根据掩模图层对图片进行裁剪
clip = gdal_array.numpy.choose(mask, (clip, 0)).astype(gdal_array.numpy.uint8)
# 将裁剪图像保存为tiff
gdal_array.SaveArray(clip, "{}.tif".format(output), format="GTIFF", prototype=raster)

目前树上是给出的上述代码,不过我在做的过程中,发现程序报错,无法打开shp或者dbf文件,目前还不知道为什么,后续有时间的话我再处理这个问题:

四、图像分类

常用的图像分类方法包括监督分类,非监督分类等。本示例采用阈值切片法进行分类。使用的数据为Landsat 8 遥感影像,下载地址为:

http://github.com/GeospatialPython/Learn/blob/master/thermal.zip?raw=true

用ENVI打开原始数据,发现该数据仅有一个波段,像元DN值介于[13559 33021]:

对上述影像进行分类,其代码为:

from osgeo import gdal_array

# 输入文件
src = "./thermal/thermal.tif"

# 输出文件名
tgt = "classified.jpg"

# 使用gdal库加载图片到numpy
srcArr = gdal_array.LoadFile(src)

# 根据类别数目将直方图分割成20个颜色区间
classes = gdal_array.numpy.histogram(srcArr, bins=20)[1]

# 颜色查找表的记录数必须是len(classes)+1,声明RGN元组
lut = [[255, 0, 0], [191, 48, 48], [166, 0, 0], [255, 64, 64], [255, 155, 155],
       [255, 116, 0], [191, 113, 48], [255, 178, 115], [0, 153, 153], [29, 115, 155],
       [0, 99, 99], [166, 75, 0], [0, 204, 0], [51, 204, 204], [255, 150, 64],
       [92, 204, 204], [38, 153, 38], [0, 133, 0], [57, 230, 57], [103, 230, 103],
       [184, 138, 0]]

# 分类初始值
start = 1

# 创建一个RGB颜色的JPEG图片输出
rgb = gdal_array.numpy.zeros((3, srcArr.shape[0], srcArr.shape[1], ), gdal_array.numpy.float32)

# 处理所有类并声明颜色
for i in range(len(classes)):
    mask = gdal_array.numpy.logical_and(start <= srcArr, srcArr <= classes[i])
    for j in range(len(lut[i])):
        rgb[j] = gdal_array.numpy.choose(mask, (rgb[j], lut[i][j]))
    start = classes[i] + 1

# 保存图片
output = gdal_array.SaveArray(rgb.astype(gdal_array.numpy.uint8), tgt, format="JPEG")
output = None

分类好的最终影像结果为:

五、图像特征提取

本示例将从Landsat 8 OLI拍摄的热红外图像中获取一组屏障岛屿区域影像子集的图片。

数据的下载地址为:

http://git.io/vqarj

原始数据只有一个波段,通过统计,发现像元DN值介于[26838 31320]。原始影像为:

本例子的目标是将影像中的3座岛屿提取出来并将其保存为shapefile文件。在执行提取操作之前会先对物馆区域进行遮罩,由于岛屿和水体形成了鲜明对比,所以这里可以采用阈值分割的方法进行处理。下面直接给出提取岛屿的代码:

from osgeo import gdal_array

# 输入文件名称
src = "./islands/islands.tif"

# 输出文件名称
tgt = "islands_classified.tif"

# 使用gdal库将图像载入numpy
srcArr =gdal_array.LoadFile(src)

# 将直方图分为20个子区间以便区分
classes = gdal_array.numpy.histogram(srcArr, bins=2)[1]

lut = [[255, 0, 0], [0, 0, 0], [255, 255, 255]]

# 分类的起始值
start = 1

# 建立输出图片
rgb = gdal_array.numpy.zeros((3, srcArr.shape[0], srcArr.shape[1]), gdal_array.numpy.float32)

# 处理所有类别并分配颜色
for i in range(len(classes)):
    mask = gdal_array.numpy.logical_and(start <= srcArr, srcArr <= classes[i])
    for j in range(len(lut[i])):
        rgb[j] = gdal_array.numpy.choose(mask, (rgb[j], lut[i][j]))
    start = classes[i] + 1

# 保存图片
gdal_array.SaveArray(rgb.astype(gdal_array.numpy.uint8), tgt, format="GTIFF", prototype=src)

提取后的效果图为:

接下来需要将提取出的岛屿另存为shapefile文件。GDAL库中有一个函数Polygonize()能够很好的处理。Polygonize()方法允许你声明一个遮罩图层并且会使用黑色作为影像过滤器,这样一来就不会将水系区域作为一个多边形提取,最终我们只会得到岛屿部分的图像。需要注意的一点是,在脚本中从源图片中拷贝了地理参照信息到shapefile文件中,这样做的目的是正确进行地理定位。将特征区域转换为shapefile文件的代码为:

from osgeo import gdal, ogr, osr

# 阈值化后的输出栅格文件名称
src = "islands_classified.tif"
# 输出的shapefile文件名称
tgt = "extract.shp"
# 图层名称
tgtLayer = "extract"
# 打开输入的栅格文件
srcDS = gdal.Open(src)
# 获取第一个波段
band = srcDS.GetRasterBand(1)
# 让gdal库使用该波段作为遮罩层
mask = band
# 创建输出的shapefile文件
driver = ogr.GetDriverByName("ESRI Shapefile")
shp = driver.CreateDataSource(tgt)
# 拷贝空间索引
srs = osr.SpatialReference()
srs.ImportFromWkt(srcDS.GetProjectionRef())
layer = shp.CreateLayer(tgtLayer, srs=srs)
# 创建dbf文件
fd = ogr.FieldDefn("DN", ogr.OFTInteger)
layer.CreateField(fd)
dst_field = 0
# 从图片中自动提取特征
extract = gdal.Polygonize(band, mask, layer, dst_field, [], None)

上述代码的运行结果为(下图中的红色轮廓):

接下来需要对shp文件简单的添加一部分属性信息,具体的代码为:

import shapefile
import pngcanvas

r = shapefile.Reader("extract.shp")
xdist = r.bbox[2] - r.bbox[0]
ydist = r.bbox[3] - r.bbox[1]
iwidth = 800
iheight = 600
xratio = iwidth / xdist
yratio = iheight / ydist
polygons = []

for shape in r.shapes():
    for i in range(len(shape.parts)):
        pixels = []
        pt = None
        if i < len(shape.parts) - 1:
            pt = shape.points[shape.parts[i]:shape.parts[i+1]]
        else:
            pt = shape.points[shape.parts[i]:]
        for x, y in pt:
            px = int(iwidth -((r.bbox[2] - x) * xratio))
            py = int((r.bbox[3] - y) * yratio)
            pixels.append([px, py])
        polygons.append(pixels)
c = pngcanvas.PNGCanvas(iwidth, iheight)
for p in polygons:
    c.polyline(p)
f = open("extract.png", "wb")
f.write(c.dump())
f.close()

六、变化监测

变化监测一般是需要获取变化前和变化后的影像,然后使用一定的方法对两幅图像进行处理,从而获得变化信息。最常用的方法无非就是图像差值处理,还有比值法,PCA等方法。下面的示例用比较简单的差值法来进行变换监测。实验需要的数据下载地址为:

飓风前的影像 http://git.io/vqa6h

飓风后的影像 http://git.io/vqaic

用ENVI打开原始数据,左边为变化前的全色影像,右边为变化后的假彩色影像,在ENVI Classic中将两幅图像进行link,从下图可以很直观的看到,大桥非常明显的被飓风毁坏:

执行变化监测的步骤大致为:用gdal_array将图片导入numpy数据中——变化前后影像作差——图像分类着色——保存图片。具体的代码为:

from osgeo import gdal, gdal_array
import numpy as np

# 飓风前影像
img1 = "./before/before.tif"
# 飓风后影像
img2 = "./after/after.tif"

# 将上述图像载入数组
arr1 = gdal_array.LoadFile(img1).astype(np.int8)
arr2 = gdal_array.LoadFile(img2)[1].astype(np.int8)

# 在图片数组上执行差值操作
diff = arr2 - arr1

# 建立类别架构并将变化特征隔离
classes = np.histogram(diff, bins=5)[1]

# 用黑色遮罩不是特变明显的变化特征
lut = [[0, 0, 0], [0, 0, 0], [0, 0, 0],
       [0, 0, 0], [0, 255, 0], [255, 0, 0]]
# 类别初始值
start = 1
# 创建输出图片
rgb = np.zeros((3, diff.shape[0], diff.shape[1], ), np.int8)

# 处理所有类别并配色
for i in range(len(classes)):
    mask = np.logical_and(start <= diff, diff <= classes[i])
    for j in range(len(lut[i])):
        rgb[j] = np.choose(mask, (rgb[j], lut[i][j]))
    start = classes[i] + 1

# 保存图片结果
output = gdal_array.SaveArray(rgb, "change.tif", format="GTiff", prototype=img2)
output = None

实验结果中, 绿色表示添加的元素,红色表示减少的元素。示例中采用的差值算法的处理结果虽然不够完美,但也能提供一种变化检测的处理思路。最终的实验结果为:

猜你喜欢

转载自blog.csdn.net/z704630835/article/details/83349551