基于GDAL的影像切图工具

1 功能需求

    对一副3857坐标系的卫星影像按照谷歌TMS进行切片,并将切片文件存储在符合MBTiles规范的SQLite数据库中

2 依赖库

GDAL ,GeoTools

3 环境搭建

    由于个人对Java语言比较熟悉,因此使用Java语言开发环境,Java工程复用原有的SpringBoot工程,然后在pom文件中添加对GeoTools库的依赖即可实现GeoTools的引入。

3.1 Windows环境下搭建GDAL Java Binding开发环境

    GDAL是当前最流行的遥感影像处理库,提供了一系列对遥感影像数据的操作函数,支持常见的影像数据格式,并提供了相应的抽象结构方便进行算法操作。GDAL的源代码是用C和C++编写的,并通过SWIG发布其他语言版本的bindings。
    首先访问http://www.gisinternals.com/下载对应版本的编译好的GDAL库,下载并解压到本地文件夹下,注意路径不能有中文。然后在计算机-环境变量Path中添加对GDAL DLL的引用。E:\soft\GIS\gdal\bin;E:\soft\GIS\gdal\bin\gdal\java
    在E:\soft\GIS\gdal\bin\gdal\java即可找到GDAL对应的JAR包,在工程中引用后即完成了GDAL的引入。完成后运行测试代码,成功显示影像信息后即代表GDAL库引入成功。

public class GDALTest {
    public static void main(String[] args) {
        gdal.AllRegister();
        String fileName = "D:\\images\\2011_output_cai.tif";
        Dataset dataset = gdal.Open(fileName, gdalconstConstants.GA_ReadOnly);
        if (dataset == null) {
            System.err.println("GDALOpen failed - " + gdal.GetLastErrorNo());
            System.err.println(gdal.GetLastErrorMsg());

            System.exit(1);
        }
        double[] ori_transform = dataset.GetGeoTransform();
        System.out.println(String.format("Origin = (%s, %s)", ori_transform[0], ori_transform[3]));
        System.out.println(String.format("Pixel Size = (%s, %s)", ori_transform[1], ori_transform[5]));
    }
}
4 切片处理流程
4.1 首先获取原始影像的地理坐标范围
double[] ori_transform = dataset.GetGeoTransform();
double latMin = ori_transform[3];
double latMax = ori_transform[3] + (yCount * ori_transform[1]);
double lonMin = ori_transform[0];
double lonMax = ori_transform[0] + (xCount * ori_transform[1]);
ReferencedEnvelope imageBound = new ReferencedEnvelope(lonMin, lonMax, latMin, latMax, crs);
4.2 获取原始影像的像素分辨率
int xCount = dataset.getRasterXSize();
int yCount = dataset.getRasterYSize();
// 原始图像东西方向像素分辨率
double src_w_e_pixel_resolution = (lonMax - lonMin) / xCount;
// 原始图像南北方向像素分辨率
double src_n_s_pixel_resolution = (latMax - latMin) / yCount;
4.3 根据原始影像地理范围求解切片行列号
int tileRowMax = Utils.getOSMTileYFromLatitude(latMin, zoom);
int tileRowMin = Utils.getOSMTileYFromLatitude(latMax, zoom);
int tileColMin = Utils.getOSMTileXFromLongitude(lonMin, zoom);
int tileColMax = Utils.getOSMTileXFromLongitude(lonMax, zoom);
4.4 求原始影像地理范围与指定缩放级别指定行列号的切片交集
double tempLatMin = tile2lat(row + 1, zoom);
double tempLatMax = tile2lat(row, zoom);
double tempLonMin = tile2lon(col, zoom);
double tempLonMax = tile2lon(col + 1, zoom);
ReferencedEnvelope tileBound = new ReferencedEnvelope(tempLonMin, tempLonMax, tempLatMin,tempLatMax, crs);
ReferencedEnvelope intersect = tileBound.intersection(imageBound);
4.5 求解当前切片的像素分辨率(默认切片大小为256*256)
// 切片东西方向像素分辨率
double dst_w_e_pixel_resolution = (tempLonMax - tempLonMin) / 256;
// 切片南北方向像素分辨率
double dst_n_s_pixel_resolution = (tempLatMax - tempLatMin) / 256;
4.6 计算交集的像素信息
// 求切图范围和原始图像交集的起始点像素坐标
int offset_x = (int) ((intersect.getMinX() - lonMin) / src_w_e_pixel_resolution);
int offset_y = (int) Math.abs((intersect.getMaxY() - latMax) / src_n_s_pixel_resolution);

// 求在切图地理范围内的原始图像的像素大小
int block_xsize = (int) ((intersect.getMaxX() - intersect.getMinX()) / src_w_e_pixel_resolution);
int block_ysize = (int) ((intersect.getMaxY() - intersect.getMinY()) / src_n_s_pixel_resolution);

// 求原始图像在切片内的像素大小
int image_Xbuf = (int) Math.ceil((intersect.getMaxX() - intersect.getMinX()) / dst_w_e_pixel_resolution);
int image_Ybuf = (int) Math.ceil(Math.abs((intersect.getMaxY() - intersect.getMinY()) / dst_n_s_pixel_resolution));

// 求原始图像在切片中的偏移坐标
int imageOffsetX = (int) ((intersect.getMinX() - tempLonMin) / dst_w_e_pixel_resolution);
int imageOffsetY = (int) Math.abs((intersect.getMaxY() - tempLatMax) / dst_n_s_pixel_resolution);
imageOffsetX = imageOffsetX > 0 ? imageOffsetX : 0;
imageOffsetY = imageOffsetY > 0 ? imageOffsetY : 0;
4.7 使用GDAL的ReadRaster方法对影像指定范围进行读取与压缩。
推荐在切片前建立原始影像的金字塔文件,ReadRaster在内部实现中可直接读取相应级别的金字塔文件,提高效率。
Band in_band1 = dataset.GetRasterBand(1);
Band in_band2 = dataset.GetRasterBand(2);
Band in_band3 = dataset.GetRasterBand(3);

int[] band1BuffData = new int[256 * 256 * gdalconst.GDT_Int32];
int[] band2BuffData = new int[256 * 256 * gdalconst.GDT_Int32];
int[] band3BuffData = new int[256 * 256 * gdalconst.GDT_Int32];

in_band1.ReadRaster(offset_x, offset_y, block_xsize, block_ysize, image_Xbuf, image_Ybuf,gdalconst.GDT_Int32, band1BuffData, 0, 0);
in_band2.ReadRaster(offset_x, offset_y, block_xsize, block_ysize, image_Xbuf, image_Ybuf,gdalconst.GDT_Int32, band2BuffData, 0, 0);
in_band3.ReadRaster(offset_x, offset_y, block_xsize, block_ysize, image_Xbuf, image_Ybuf,gdalconst.GDT_Int32, band3BuffData, 0, 0);
4.8 将切片数据写入文件
// 使用gdal的MEM驱动在内存中创建一块区域存储图像数组
Driver memDriver = gdal.GetDriverByName("MEM");
Dataset msmDS = memDriver.Create("msmDS", 256, 256, 4);
Band dstBand1 = msmDS.GetRasterBand(1);
Band dstBand2 = msmDS.GetRasterBand(2);
Band dstBand3 = msmDS.GetRasterBand(3);

// 设置alpha波段数据,实现背景透明
Band alphaBand = msmDS.GetRasterBand(4);
int[] alphaData = new int[256 * 256 * gdalconst.GDT_Int32];
for (int index = 0; index < alphaData.length; index++) {
    if (band1BuffData[index] > 0) {
        alphaData[index] = 255;
    }
}
// 写各个波段数据
dstBand1.WriteRaster(imageOffsetX, imageOffsetY, image_Xbuf, image_Ybuf, band1BuffData);
dstBand2.WriteRaster(imageOffsetX, imageOffsetY, image_Xbuf, image_Ybuf, band2BuffData);
dstBand3.WriteRaster(imageOffsetX, imageOffsetY, image_Xbuf, image_Ybuf, band3BuffData);
alphaBand.WriteRaster(imageOffsetX, imageOffsetY, image_Xbuf, image_Ybuf, alphaData);

String tileFolder = System.getProperty("java.io.tmpdir");
String pngPath = tileFolder + File.separator + zoom + "c" + col + "r" + row +".png";
// 使用PNG驱动将内存中的图像数组写入文件
Driver pngDriver = gdal.GetDriverByName("PNG");
Dataset pngDs = pngDriver.CreateCopy(pngPath, msmDS);

// 释放内存
msmDS.FlushCache();
pngDs.delete();
4.8 读取临时文件,并转换成二进制存储到SQLite
try {
    InputStream fis = new FileInputStream(tileFile);
    byte[] imgBytes = toByteArray(fis);

    String uuid = UUID.randomUUID().toString();
    PreparedStatement mapPs = conn.prepareStatement("insert into map(zoom_level,tile_column,tile_row,tile_id) values (?,?,?,?)");
    PreparedStatement imagesPs = conn.prepareStatement("insert into images(tile_data,tile_id) values (?,?)");

         imagesPs.setBytes(1, imgBytes);
         imagesPs.setString(2, uuid);

         mapPs.setInt(1, zoom);
         mapPs.setInt(2, col);
         mapPs.setInt(3, row);
         mapPs.setString(4, uuid);

         imagesPs.executeUpdate();
         mapPs.executeUpdate();

         imagesPs.close();
         mapPs.close();
         fis.close();
         tileFile.delete();
} catch (Exception e) {
    e.printStackTrace();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342