接手一批新入库的栅格瓦片时,我最怕的不是数据量太大,而是表里到底装了什么完全没底。范围可能重叠、坐标系可能混用、有些块甚至没有地理参考信息,这种问题用肉眼根本看不完,必须靠SQL直接问数据库。PostGIS里正好有一批专门读取栅格基本信息的函数,ST_GeoReference、ST_Width、ST_Height、ST_NumBands、ST_SRID都是最常用的几个。我在一个遥感数据收尾项目里正好把这9个函数全部过了一遍,并把它们整理成了一套规范化的体检查询脚本。这篇先用一篇的篇幅讲清楚整体思路和9个函数的细节,包括它们各自解决什么问题、返回什么格式、实际使用中要注意哪些边界情况。适合刚接触PostGIS栅格数据的开发者、GIS数据分析师,以及那些正在为栅格入库后的数据质量发愁的人。
1. 内容整体设计与思路拆解
1.1 为什么栅格数据需要先做“基本信息读取”
很多项目里栅格不是一整张大tif直接丢进数据库,而是被切成了几百上千个瓦片块,一行记录就是一个小块。这样做的目的是方便按范围检索、按需加载,也让并行计算成为可能。但代价是管理难度上去了:你很难像打开一张图片那样快速了解这批数据。
以前我处理过一批分块入库的影像,光看文件名完全看不出问题。有的块SRID不对,有的块NoData值和其他块对不上,还有的块像素尺寸异常。如果靠一个个打开查看,根本不现实。用SQL读取栅格元数据就成了最高效的方案:一条查询扫全表,尺寸、波段、投影、像素类型全部列出来,任何异常都逃不过眼睛。
PostGIS对栅格的支持来自GDAL,所有栅格函数都遵循GDAL的数据模型。这意味着函数背后是有统一标准的,不是不同版本乱写。理解了这个底层逻辑,你就知道为什么有的函数返回text、有的返回int、有的返回double——它们本质上是把GDAL元数据模型里不同字段暴露给你而已。
1.2 九个函数的选用逻辑:从三个维度看栅格
9个函数听起来多,但归类后很好记。我自己习惯把栅格的基本信息分成三个维度:定位与几何、像素尺寸、波段属性。
- 定位与几何:ST_GeoReference(六参数地理参考)、ST_PixelWidth(像素宽度)、ST_PixelHeight(像素高度)
- 像素尺寸:ST_Width(一行有多少像素)、ST_Height(一列有多少像素)
- 波段属性:ST_NumBands(波段数)、ST_BandPixelType(波段像素类型)、ST_BandNoDataValue(波段无数据值)
- 空间基准:ST_SRID(空间参考ID)
这套组合拳打完之后,一块栅格的分辨率、尺寸、波段结构、坐标系、NoData值就全部清楚了。你拿到任何一张陌生的栅格表,先跑一遍这些查询,数据质量心里就有数了。
顺带说一句,还有一个聚合函数ST_Extent经常和它们一起出现,用来查看整表所有瓦块的总范围。但严格来说它不在标题这个“9个函数”的清单里,所以我会放在第三部分实操里作为补充讲解。
2. 核心函数逐个拆解:参数、返回值与边界
2.1 ST_GeoReference:最能代表栅格身份的函数
ST_GeoReference是这批函数里信息量最大的一个。它直接读取栅格的仿射变换参数,告诉你这片栅格放在地球表面的什么位置、每个像素占多大地面范围。
语法很简单:
ST_GeoReference(raster) -- 默认返回 GDAL 格式 ST_GeoReference(raster, format) -- format 可选 'GDAL' 或 'ESRI'默认的GDAL格式返回6行文本,每行一个数值。我拿一块10米分辨率、左上角坐标在某个位置的数据为例:
SELECT ST_GeoReference(rast) FROM raster_demo WHERE rid = 1;输出结果:
10.0000000000 0.0000000000 0.0000000000 -10.0000000000 372314.5000000000 2679018.5000000000这6行对应关系如下:
| 行号 | 含义 | 本例值 |
|---|---|---|
| 第1行 | 像素宽度(X方向缩放) | 10 |
| 第2行 | X方向旋转系数 | 0 |
| 第3行 | Y方向旋转系数 | 0 |
| 第4行 | 像素高度(Y方向缩放,常为负) | -10 |
| 第5行 | 左上角X坐标 | 372314.5 |
| 第6行 | 左上角Y坐标 | 2679018.5 |
第4行是负值,这个很多人刚接触时不明白。简单说,栅格图片的坐标原点在左上角,行号向下递增,而地理坐标系的Y轴通常是向上递增的,所以Y方向缩放必须取负,才能让“向下走一行”对应“地理Y坐标减小一个像素高度”。如果这个值是正的,那你这个栅格实际上是被上下翻转了。
如果指定ESRI格式,返回的是4行,对应世界文件(world file)的内容:
SELECT ST_GeoReference(rast, 'ESRI') FROM raster_demo WHERE rid = 1;返回:
10.0000000000 -10.0000000000 372314.5000000000 2679018.5000000000GDAL格式和ESRI格式不只行数不同,坐标含义也有细微差别,这个我放在第四部分详细说,因为它是实际踩坑重灾区。
2.2 尺寸与分辨率:ST_Width、ST_Height、ST_PixelWidth、ST_PixelHeight
四个函数经常成对出现,前两个描述瓦片的像素网格大小,后两个描述每个像素在地面上的实际大小。
ST_Width(raster) -- 返回 integer,栅格的像素列数 ST_Height(raster) -- 返回 integer,栅格的像素行数 ST_PixelWidth(raster) -- 返回 double precision,像素宽度(投影单位/像素) ST_PixelHeight(raster) -- 返回 double precision,像素高度(投影单位/像素)举个例子:
SELECT ST_Width(rast) AS cols, ST_Height(rast) AS rows, ST_PixelWidth(rast) AS pixel_width, ST_PixelHeight(rast) AS pixel_height FROM raster_demo WHERE rid = 1;返回类似:
cols | rows | pixel_width | pixel_height ------+------+-------------+------------- 256 | 256 | 10 | 10这意味着这块瓦片是256列乘256行的像素网格,每个像素在X方向和Y方向各占10个投影单位。如果空间参考是米制投影,那就是10米分辨率。
需要特别留意的点:ST_Width和ST_Height返回的是“这个raster对象”的尺寸,不是整幅影像的尺寸。PostGIS里栅格被分块存储后,边缘位置的瓦块很可能不是完整的256x256,比如最右一列可能是128x256,最下面一行可能是256x96。很多人在做统计时发现宽高有变化,第一反应是数据坏了,实际上很可能只是边界块。
ST_PixelWidth和ST_GeoReference返回的第1行是不是一回事?基本是,但ST_PixelWidth在实现上做了处理,直接给你一个便于比较的正值。ST_PixelHeight同理,返回的是投影单位下的像素高度,一般也是正数。我可以把它们理解为“把GeoReference里的缩放系数单独提出来解释给你听”。
2.3 波段属性:ST_NumBands、ST_BandPixelType、ST_BandNoDataValue
栅格除了空间位置,还有波段的概念。常见影像可能是单波段(如DEM)或多波段(如遥感影像的RGB或更多波段)。这三个函数负责把波段结构摸清楚。
ST_NumBands(raster) -- 返回 integer,波段总数 ST_BandPixelType(raster, band) -- 返回 text,指定波段的像素类型 ST_BandNoDataValue(raster, band) -- 返回 double precision,指定波段的NoData值注意波段索引从1开始,不是从0开始。这一点和很多编程语言的数组习惯不一样,容易写错。
SELECT ST_NumBands(rast) AS band_count, ST_BandPixelType(rast, 1) AS band1_type, ST_BandNoDataValue(rast, 1) AS band1_nodata FROM raster_demo WHERE rid = 1;像素类型的取值来自GDAL的数据类型定义,常见的有:
| 类型代码 | 含义 |
|---|---|
| 1BB | 1位二值 |
| 8BUI | 8位无符号整型 |
| 16BSI | 16位有符号整型 |
| 16BUI | 16位无符号整型 |
| 32BSI | 32位有符号整型 |
| 32BUI | 32位无符号整型 |
| 32BF | 32位浮点 |
| 64BF | 64位浮点 |
不同像素类型直接影响后续处理的精度和性能。比如16BSI最多表示-32768到32767,如果原始数据是海拔或温度这类可能为负的值,用8BUI就不合适。我遇到过一批DEM数据,入库时被统一转成了16BSI,没问题;但另一批却被当成8BUI存了,导致负海拔全被截断。这个函数就是你排查这类问题的第一道防线。
NoData值也需要解释一下。栅格影像经常有无效区域,比如遥感影像边缘的黑边、DEM里无法测量的空洞,这些区域在数据里用某个特定值填充,这个值就是NoData。读取它非常关键,因为后续做统计分析时会直接决定“有效像元”范围。
2.4 空间基准与范围:ST_SRID与补充的ST_Extent
ST_SRID返回栅格的空间参考ID。它和矢量数据的ST_SRID逻辑一致,不再多讲,只有一个点需要强调:如果查询结果是0,说明这个栅格没有定义坐标系。这种情况比坐标系不一致还麻烦,因为连坐标含义都无法判断。
SELECT ST_SRID(rast) FROM raster_demo WHERE rid = 1;补充说说ST_Extent。严格讲它是聚合函数,作用于整表而不是单个raster对象:
SELECT ST_Extent(rast) FROM raster_demo;返回类似:
BOX3D(371234.5 2670018.5 0, 380114.5 2678918.5 0)它把整表所有瓦块的范围合并成一个box,一眼就能看出这批数据的经纬范围。上面那个“0”是Z维度,没有高程信息时默认取0。如果范围明显超出预期,说明某些瓦块位置错乱,这时需要用ST_SRID和ST_GeoReference联合排查。
3. 实操:从导入到体检的完整流程
3.1 准备数据:用raster2pgsql导入一张测试栅格
纸上谈兵没有意义,我直接演示怎么从零开始建一张栅格表。PostGIS导入栅格最常用的工具是raster2pgsql,它可以把tif文件切片并生成SQL脚本。
假设我有一张叫sample_tif.tif的栅格文件,目标是导入到public模式下的raster_demo表:
raster2pgsql -s 3857 -t 256x256 -C -I -M sample_tif.tif public.raster_demo | psql -U postgres -d gisdb参数说明:
-s 3857:指定SRID为3857(Web墨卡托投影)-t 256x256:切成256列乘256行的瓦块-C:生成栅格约束,这样后续可以在raster_columns视图里看到表级元数据-I:为栅格列创建GIST空间索引-M:导入完成后执行VACUUM ANALYZE,更新统计信息
这一步之后,raster_demo表里每一行就是一块256x256的瓦片,最后一列和最后一行可能是残缺块,这属于正常现象。
3.2 单块瓦片信息读取:给一块栅格做全面体检
导入完成后,先挑一块瓦片看全貌:
SELECT rid, ST_Width(rast) AS pixel_cols, ST_Height(rast) AS pixel_rows, ST_NumBands(rast) AS num_bands, ST_BandPixelType(rast, 1) AS band1_type, ST_BandNoDataValue(rast, 1) AS band1_nodata, ST_SRID(rast) AS srid, ST_PixelWidth(rast) AS pixel_width, ST_PixelHeight(rast) AS pixel_height, ST_GeoReference(rast, 'ESRI') AS world_file FROM raster_demo WHERE rid = 1;这段SQL基本覆盖了9个函数里的8个。返回值里如果srid显示3857、像素宽度和高度都是10、波段类型是16BSI、NoData是-9999,那这块瓦片的信息就完整了。
我的习惯是先看这一条记录,确认没有明显异常后再去做全表统计。如果单块都有问题,全表统计出来的数字也没有意义。
3.3 全表数据质量统计:哪些瓦片有问题一眼暴露
单块体检没问题后,就可以跑全表聚合了。这一轮的目标不是看某个值,而是看整批数据的一致性。
SELECT COUNT(*) AS total_tiles, COUNT(DISTINCT ST_SRID(rast)) AS srid_count, MIN(ST_Width(rast)) AS min_width, MAX(ST_Width(rast)) AS max_width, MIN(ST_Height(rast)) AS min_height, MAX(ST_Height(rast)) AS max_height, COUNT(DISTINCT ST_NumBands(rast)) AS band_count_variants FROM raster_demo;解释一下怎么看结果:
- total_tiles是瓦块总数,用来确认导入数量是否符合预期
- srid_count如果大于1,说明表里混入了不同坐标系的数据,这是严重问题
- min_width和max_width如果不一致,说明存在边界残缺块。但如果min是256而max变成512,那就不是边界问题了,而是源数据本身分辨率不一致,需要警惕
- band_count_variants大于1说明不同瓦片的波段数不一样,会导致后续按波段处理时出错
再结合像素类型做一次分组统计:
SELECT ST_BandPixelType(rast, 1) AS pixel_type, COUNT(*) AS tile_count FROM raster_demo GROUP BY ST_BandPixelType(rast, 1);正常情况下应该只有一种像素类型。如果出现两种以上,十有八九是多个来源的数据混着入库了。这种问题后期处理时很难发现,因为很多函数不会直接报错,而是默默做了类型转换,直到结果偏差你才反应过来。
3.4 查看表级元数据:raster_columns视图是很好的补充
9个函数操作的是单个raster对象,但PostGIS还提供了一张视图叫raster_columns,专门查看整张栅格表的约束级元数据。条件是导入时必须用了-C生成约束。
SELECT r_table_name, srid, scale_x, scale_y, blocksize_x, blocksize_y, num_bands, pixel_types, nodata_values, extent FROM raster_columns WHERE r_table_name = 'raster_demo';这张视图的价值在于:它不扫描每一行瓦块,而是直接读取约束信息,速度非常快。pixel_types和nodata_values在视图里是数组类型,比如{16BSI}或{-9999},正好对应各波段。
为什么我强调它是补充?因为raster_columns来自约束,而约束可能缺失。如果导入时没用-C,这张视图里就不会有记录。这时候9个函数反而是最可靠的,因为它们是直接读取栅格内部的元数据,不受约束影响。
3.5 筛选异常瓦块的常用写法
体检发现问题之后,要快速定位到具体哪些瓦块有问题。几个常用筛查写法:
查出SRID和预期不一致的瓦块:
SELECT rid, ST_SRID(rast) FROM raster_demo WHERE ST_SRID(rast) <> 3857;查出尺寸异常的瓦块(注意区分边界块):
SELECT rid, ST_Width(rast), ST_Height(rast) FROM raster_demo WHERE ST_Width(rast) > 256 OR ST_Height(rast) > 256;查出NoData值不一致的瓦块:
SELECT rid, ST_BandNoDataValue(rast, 1) FROM raster_demo WHERE ST_BandNoDataValue(rast, 1) <> -9999;这些查询我自己用得最多。每次数据交接,我都会先跑一遍这几条,确认全表数据一致性没问题后,才敢继续做后续的影像拼接、统计分析或者服务发布。
4. 常见问题与排查技巧实录
4.1 GDAL格式与ESRI格式混用导致的坐标偏移
这是我在实际项目中踩过最深的坑。ST_GeoReference默认返回GDAL格式,也就是6参数;指定'ESRI'后返回4参数。表面看只是行数不同,但两种格式的坐标含义差半个像素。
GDAL格式的仿射变换通常把左上角坐标理解为像素外边框的左上角,而ESRI世界文件格式把坐标理解为左上角像素的中心。当分辨率为10米时,两种格式换算出的左上角坐标可能相差10米的二分之一,也就是5米。如果你在外部工具里用一种格式读取坐标,却用另一种格式的约定去解析,出来的位置就会偏移半个像素。
我的建议是:整条数据链路统一使用一种格式。如果是内部使用,默认GDAL格式就好;如果要和其他GIS软件交换,明确对方需要的是世界文件,就用'ESRI'格式,同时不要混用两种格式去对比坐标值。
4.2 分块尺寸不等于整图尺寸
很多新手第一次统计ST_Width时发现结果大多是256,少数是128或其他值,心里就开始发慌。其实只要搞懂PostGIS的栅格分块机制,就明白这是正常的。
raster2pgsql在切块时按固定大小切,比如256x256。如果影像总宽度是1000像素,那么最后一列的宽度只有1000减3个整块再减一个块?算一下:1000/256得3个整块,余232像素,所以最后一列是232像素宽。这是正常现象。真正需要警惕的是宽度和高度大于256的情况,那说明某些瓦片没有按照预期分块。
判断边界块的技巧是结合ST_Envelope查看该瓦片的地理范围。如果边界块的范围落在整幅影像范围内,且尺寸小于256,基本不用担心。如果尺寸大于256,就要回去检查导入参数。
4.3 NoData值不统一对后续计算的深远影响
NoData值不统一这个问题,表面看只是数字不整齐,影响其实很大。PostGIS后续很多统计函数比如ST_SummaryStats、ST_Value都依赖NoData来判断哪些像元有效。
比如整表里大多数瓦块的NoData是-9999,但有一块是0。在做均值统计时,那一块的无效区域就会被当成数值0参与计算,直接拉低整体均值。更麻烦的是,这种误差不会报错,你会拿到一个看起来合理但实际错误的统计结果。
我建议在导入阶段就用raster2pgsql -N参数强制指定NoData值,从源头统一。如果数据已经入库了,就写个查询扫一遍,把NoData不一致的块列出来,逐块修正。这事情没有捷径,但至少可以先知道问题范围。
4.4 SRID为0的瓦块怎么处理
ST_SRID返回0,常见原因有两个:源文件本身不带投影信息,或者导入时没有用-s指定SRID。前一种情况需要你根据元数据手工确认投影后再设置;后一种情况是操作失误,补一步即可。
补丁写法:
UPDATE raster_demo SET rast = ST_SetSRID(rast, 3857) WHERE ST_SRID(rast) = 0;但这里有个必须注意的前提:你确认这些瓦块的坐标数值确实是在3857坐标系下的坐标,而不是其他坐标系或者经纬度。如果搞错了,强制设置SRID只会把错误固化下来。我处理过一个案例,一批数据本身是4326经纬度,却被人设置成了3857,结果所有位置在Web地图上偏出去几百公里。所以动手之前,务必拿一个已知坐标点做交叉验证。
4.5 像素类型不一致的处理原则
像素类型不一致时,最稳妥的做法是在入库阶段统一。比如用raster2pgsql导入时,可以借助-R、-T这类参数?其实更可靠的做法是在导出或预处理阶段用GDAL翻译工具统一转换后再入库。
PostGIS里也有ST_Reclass这样的重分类函数可以改变像素值,但它是逐像元计算,处理大表时性能一般,适合小范围修正,不适合全表转换。所以处理原则是:源头统一优于入库后修补。如果必须修补,先备份数据再动手,避免转换过程中把像素范围截断造成更大损失。
我个人在实际项目里习惯把单块体检SQL封装成一个视图,比如v_raster_health,每次拿到新数据直接SELECT就能看到第一批瓦块的状态。很多看起来古怪的问题,比如某次导出后数据范围偏移、某个分块投影丢失,靠这些查询都能第一时间锁定。这套“先体检、再处理”的思路救过我不少次,也建议你每次栅格数据入库后先跑一遍再继续。下一篇我会继续讲如何用ST_Value、ST_SummaryStats这些函数做像素级统计,把栅格数据的价值真正榨出来。