简介:《夜间灯光的提取及强度计算》面向GIS、遥感与城市规划相关学习者,围绕夜间灯光栅格数据与行政矢量数据的配合使用,解决特定区域灯光提取、强度统计及可视化表达等问题。压缩包共1个docx文件,大小约6.52MB,以图文文档形式整理操作要点,便于按主题查阅和复盘。已有3653人学习,说明该主题在ArcGIS实操与空间分析学习中具有一定关注度。文档内容覆盖按属性选择目标行政区、按掩膜提取灯光栅格、以表格显示分区统计计算区域灯光强度,以及连接统计结果并用分级色彩制图展示差异,可帮助读者理解从数据准备到结果表达的完整分析思路,适合课程作业、专题练习与城市灯光遥感应用入门参考。
1. 从一整幅灯光栅格到各区县强度排名:这套流程解决什么问题
手上拿到一份覆盖全省的年度夜间灯光栅格,要交的成果是“A 县、B 区这几个区县谁亮谁暗,排个序,出一张图”。整幅影像和目标行政区划图之间,实际上隔着三个操作:把目标区县的范围从整幅栅格里抠出来、把像元亮度按行政区汇总成一个数、把这个数挂回行政矢量图上出图。这套操作在 GIS 里对应 ArcGIS 的两个工具——按掩膜提取和以表格显示分区统计,工具本身参数不多,真正的麻烦在坐标系对不齐、像元被悄悄重采样、连接字段类型不匹配这些细节上,任何一处出问题,结果都会偏,而且从图上还看不出来。这篇按实际动手顺序走一遍:数据怎么准备、掩膜提取的参数怎么设、统计类型怎么选、连接和符号化怎么做,最后把常见的翻车点集中列出来。适合需要按行政区出灯光强度表、把灯光当作城市化或经济活动代理指标的从业者。
2. 数据准备:坐标系和属性表没理顺,后面全是无用功
拿到数据先不要急着打开 ArcToolbox。夜间灯光这类栅格产品和行政矢量来自不同渠道,坐标系、字段命名、几何质量都带着各自的习惯,直接往工具里怼,报错还算好的,最怕的是不报错但结果悄悄偏掉。这一章做三件事:检查并统一坐标系、用属性查询把目标区县挑出来、把挑出来的要素导成干净的新文件。
2.1 坐标系对齐:统一到栅格,别统一到矢量
夜间灯光栅格产品常见两种坐标形式:一种是 WGS84 经纬度(单位是度),一种是厂商预处理过的投影坐标(单位是米);行政矢量在国土、规划口子里常见 CGCS2000 或地方高斯投影。ArcGIS 的内容列表里,不同坐标系的图层会“看起来”叠在一起,这是动态投影在起作用,但工具执行时会按各自真实坐标去处理,掩膜和栅格可能在空间上错开几百米,表现就是提取结果边界偏移、目标区缺一块,甚至整块区域全是 NoData。
统一的方向有个原则:把矢量投影到栅格的坐标系,不要反过来把栅格投影到矢量。原因是栅格做投影变换必须重采样,双线性或三次卷积会把原本的整数 DN 值插值成小数,零值区和低亮度区的值会被邻居拉高,强度统计从此失真;而矢量投影只改坐标值,不动属性,代价小得多。常见做法是打开“投影”工具(Data Management Tools → Projections and Transformations → Project),输出坐标系直接选栅格那一套。
用脚本批量判断更省事,多份数据一起进来时尤其明显:
# -*- coding: utf-8 -*- import arcpy light = r"E:/nightlight/NL_2020.tif" bound = r"E:/boundary/county.shp" # 用 factoryCode 做精确比对,不要只比 name sr_light = arcpy.Describe(light).spatialReference sr_bound = arcpy.Describe(bound).spatialReference print("栅格:", sr_light.name, sr_light.factoryCode) print("矢量:", sr_bound.name, sr_bound.factoryCode) # 不一致就把矢量投影到栅格坐标系 if sr_light.factoryCode != sr_bound.factoryCode: out_shp = r"E:/boundary/county_prj.shp" arcpy.Project_management(bound, out_shp, sr_light) print("已投影输出:", out_shp)Describe().spatialReference拿到的是一个空间参考对象,直接作为Project_management的第三个参数传进去,比手填坐标系名字可靠。这里强调用factoryCode而不是name比对:两个基准面不同但名称里都带类似字样的坐标系并不少见,只比名字容易漏判。另外补一句,如果栅格本身是经纬度坐标,后面分区统计输出的面积字段单位会是“平方度”,这个数值没有任何业务含义;真要算灯光密度,得先把栅格投到等积投影再统计,或者干脆只用 SUM/MEAN 而不用面积字段做分母。
还有一个容易被忽略的点:坐标系统一之后,最好顺手确认一下掩膜面和栅格是不是真的重叠。做法是把两个图层都加到地图里,缩放至目标区县,用“识别”工具在边界上点几个位置,看栅格值是否能正常读出。如果读出来是 NoData,说明两个数据虽然在同一个坐标系下,但实际覆盖范围根本没交集,这种情况多半是数据本身就不是同一套地理范围,硬跑工具只会得到一张空表。
2.2 属性选择:区县名字里的空格和全半角是第一道坎
目标区县往往不是一个,而是十几个。用“按属性选择”(Select By Attributes)拼 SQL 是标准动作,但拼之前先做两件事:打开属性表,对名称字段排一次序,看有没有重名(同名区县跨地市很常见),再看有没有前后空格、全角括号、异体字。这些脏数据会让name = 'A县'这种精确匹配直接选空。
字段名和值的引号规则要记牢:shapefile 的字段名用双引号包住,字符串值用单引号;文件地理数据库里的字段名可以不加引号,但加了也不报错。多条条件用OR连接,写法是:
"name" = 'A县' OR "name" = 'B区' OR "name" = 'C区'不确定名字写得对不对时,先用模糊匹配探一下,再改成精确条件:
"name" LIKE '%A%'选中之后不要急着导出,先看状态栏显示的选择条数对不对。选多了通常是OR优先级写错或者某个名字匹配到了两个区县;选少了多半是空格问题,可以在字段计算器里对名称字段做一次去空格处理(Python 解析器下写!name!.strip()),再重新选。字段计算器这一步建议在副本上做,原始数据保持不动,出了问题还有退路。
如果要按多个条件组合筛选,比如既限定地级市又限定区县,记得把每组条件用括号包起来,("city" = 'X市') AND ("name" = 'A县' OR "name" = 'B区'),否则AND和OR的优先级会让你选出意料之外的结果。这是血泪经验,选完一定要核对数量。
2.3 导出所选要素:字段留全,路径干净
选中的要素要单独导成一个面文件,作为后面掩膜提取的输入。操作路径是右键图层 → 数据 → 导出数据,在对话框里勾选“所选要素”,输出位置选文件地理数据库(.gdb)比 shapefile 稳,字段名长度不受 10 字符限制,中文也不容易乱码。
| 导出参数 | 建议设置 | 原因 |
|---|---|---|
| 输出位置 | 文件地理数据库 | 字段名不截断,中文编码稳定 |
| 输出要素类名 | 全英文、无空格,如county_sel | 避免工具报路径类错误 |
| 仅导出所选要素 | 勾选 | 不勾会把整省边界带进去 |
| 输出坐标系 | 与灯光栅格一致 | 避免二次投影 |
| 保留字段 | 名称、行政区代码、上级市代码 | 后面连接和分组都要用 |
导出之后打开新图层的属性表确认两件事:行数等于刚才选中的数量;名称字段没有变成乱码。这两条都过了,数据准备这一关才算过。很多人图省事直接拿原始全省图层当掩膜,结果统计表里多出几十行不需要的区县,后面再筛一遍反而更费时间。
3. 按掩膜提取:能用面抠就别用矩形框凑合
数据准备完,进入裁剪环节。ArcGIS 里能“裁”栅格的不止一个工具,选错了不会报错,但统计结果会系统性偏高,而且偏得很隐蔽。这一章说清楚掩膜提取和裁剪的区别、参数逐项怎么设、跑完之后看哪几个数判断结果对不对。
3.1 掩膜提取和裁剪的差别:差的是面积口径
“裁剪”(Clip)按输入要素的外接矩形裁,矩形以内的像元全保留;行政区划边界基本都不是矩形,外接矩形会把相邻区县的灯光一并框进来。如果下游要算 SUM 或者求平均值,多进来的这部分灯光会直接抬高结果,边界越不规则、邻区越亮,偏差越大。
“按掩膜提取”(Extract by Mask)按面裁,落在面外的像元统一置为 NoData,只有面内的像元参与后续统计。夜间灯光按行政区汇总这类任务,必须用掩膜提取。
| 对比项 | 裁剪(Clip) | 按掩膜提取(Extract by Mask) |
|---|---|---|
| 输出范围 | 输入要素外接矩形 | 输入要素几何本身 |
| 面外像元 | 保留 | 置为 NoData |
| 是否改变像元值 | 不改(除非重采样) | 不改(除非重采样) |
| 适用场景 | 快速缩小处理范围 | 按行政区、流域等不规则面统计 |
实际工具入口在 Spatial Analyst 工具 → 提取分析 → 按掩膜提取。输入栅格填整幅灯光 tif,输入掩膜填上一步导出的目标区县面,输出栅格给一个全英文路径。这一步没有太多可调参数,真正决定结果质量的是环境设置。
3.2 参数与环境设置:捕捉栅格和像元大小必须锁死
掩膜提取本身不复杂,翻车基本都出在环境变量没设。ArcGIS 默认会根据掩膜范围自动推算输出栅格的像元大小和对齐方式,一旦输出像元和原始栅格错开半个像元,工具就会做一次重采样,DN 值被插值成小数,低亮度区的值被邻域拉高,强度统计随之失真。这类偏差在图上完全看不出来,只有对比数值才发现。
用脚本跑时把四个环境变量显式写死:
# -*- coding: utf-8 -*- import arcpy from arcpy.sa import ExtractByMask arcpy.env.overwriteOutput = True arcpy.env.workspace = r"E:/work" light = r"E:/nightlight/NL_2020.tif" mask = r"E:/work/county_sel.shp" # 锁定对齐基准,防止输出像元漂移和重采样 arcpy.env.snapRaster = light arcpy.env.cellSize = light arcpy.env.outputCoordinateSystem = arcpy.Describe(light).spatialReference arcpy.env.extent = light out = ExtractByMask(light, mask) out.save(r"E:/work/nl_mask.tif")snapRaster是这里最关键的一行,它保证输出的像元行列和原始栅格严格对齐,不会出现半像元偏移;cellSize直接引用原始栅格对象,等价于“输出像元大小 = 输入像元大小”,避免工具按掩膜范围自动缩放出更细的网格;extent设成整幅栅格,是为了让工具不去裁掉不该裁的部分,掩膜的面范围由掩膜要素自身决定。用图形界面操作时,这几项在“环境设置”里同样要手动填,默认值是“按输入自动”,别信默认。
输出格式建议直接.tif,不要输出到个人地理数据库(.mdb),后者对单文件大小有限制,全省范围的灯光栅格很容易超出。
3.3 跑完先看三个数,再谈下一步
提取结果出来别急着做统计,先做三项自检,成本很低但能拦掉大部分问题。
第一项,比较最大值。打开原始栅格和掩膜结果的图层属性 → 源,看两者的最大值。掩膜结果的像元值应该是原始像元值的子集,最大值不可能超过原始最大值。如果超过了,说明中间发生过拉伸或者重采样,回去查环境设置。
第二项,看 NoData 和有效像元数。掩膜结果里,面外是 NoData,面内是有效值。有效像元数乘以像元面积,应该和掩膜面的投影面积大致相当(允许边界锯齿带来的少量误差)。差距过大,通常是坐标系没统一或者掩膜面本身有自相交、多部件问题。
第三项,放大边界看贴合度。把两个图层都加载进来,缩放到区县边界,切换几个位置看栅格边缘是不是贴着矢量边线走。如果出现明显的锯齿状错位或者半像元的偏移,说明捕捉栅格没设对。这一步靠肉眼就能判断,比事后查表快得多。
4. 分区统计:SUM 和 MEAN 选错,排名直接翻车
掩膜结果只是“目标区县范围内的灯光像元”,要变成“每个区县一个强度数值”,靠的是以表格显示分区统计。这个工具的输入输出都是明确的,难的是统计类型怎么选、统计口径怎么统一、结果怎么挂回矢量图。三件事任何一件做错,最后那张分级色彩图都会骗人。
4.1 参数逐项拆:区域字段别用名称
工具路径是 Spatial Analyst 工具 → 区域分析 → 以表格显示分区统计。五个关键参数:
| 参数 | 填什么 | 说明 |
|---|---|---|
| 输入栅格数据或要素区域数据 | 目标区县面 | 必须和掩膜用的是同一份 |
| 区域字段 | 行政区代码或 OBJECTID | 唯一标识,不能用名称 |
| 输入赋值栅格 | 掩膜后的灯光栅格 | 不是原始整幅栅格 |
| 输出表 | 文件地理数据库里的表 | 不要放临时目录 |
| 统计类型 | ALL 或按需勾选 | 一次全出省得跑多遍 |
区域字段这一项值得单独说。很多人图直观选名称字段,结果遇到两个同名区县,统计表里就合并成一行,两个区的灯光被加在一起算。用行政区代码或者 OBJECTID 这类唯一值,虽然可读性差一点,但不会出错。如果手头只有名称字段,导出前先加一个自增的短整型字段(字段计算器里用!OBJECTID!或顺序编号),拿它当区域字段。
统计类型选 ALL 最省事,SUM、MEAN、MAX、MIN、STD、COUNT 一次全出,后面想换口径不用重跑工具。勾选“忽略 NoData”这一项,否则包含 NoData 的区域整行会被跳过。
脚本写法:
# -*- coding: utf-8 -*- import arcpy from arcpy.sa import ZonalStatisticsAsTable zone = r"E:/work/county_sel.shp" value = r"E:/work/nl_mask.tif" out_table = r"E:/work/zonal_stat.dbf" # 统计类型 ALL:SUM/MEAN/MAX/MIN/STD/COUNT 一次全出 ZonalStatisticsAsTable(zone, "ADCODE", value, out_table, ignore_nodata="DATA", statistics_type="ALL") print("统计表已生成:", out_table)第二个参数"ADCODE"就是区域字段,必须和矢量属性表里的字段名完全一致,大小写也要一致。ignore_nodata="DATA"表示只忽略 NoData 像元,不忽略零值像元。夜间灯光里 DN 为 0 的像元代表“无灯光”,是有意义的观测值,不能当缺失值剔除,所以这一项不能设成"NODATA",否则会把建成区边缘的暗区整片丢掉。
4.2 统计口径:面积不同的区县,SUM 不能直接比
统计表出来之后,最常被问的问题是“到底用哪个字段排序”。这取决于你要表达什么。
SUM 是该区域内所有像元亮度之和,反映的是“灯光总量”,适合做经济规模、建设用地总量的代理指标,但它和区域面积强相关,面积大的区县天然占优,拿它直接给区县排名不公平。
MEAN 是平均值,消除了面积影响,反映“平均亮度”,适合比较不同区县的开发强度。缺点是如果区县边界内包含大片山地、水域这些无灯光区域,平均值会被拉低,看起来比实际“暗”。
COUNT 是参与统计的有效像元数,通常不直接用于制图,但它是质量控制的好帮手:COUNT 乘以像元面积应该接近该区县的投影面积,差距过大说明掩膜有问题。
STD 反映区域内亮度的离散程度,城乡差距大的区县 STD 高,这个指标在分析“灯光分布是否均衡”时有用,单纯排名一般不用。
如果一定要用 SUM 做横向比较,先做个归一化:SUM 除以区域面积,得到“灯光密度”,单位是 DN/平方公里,这个口径兼顾了总量和面积,实际项目里用得最多。归一化在属性表里加一个双精度字段,字段计算器里直接写!SUM! / !Shape_Area! * 1000000(面积单位是平方米,乘 1000000 换算成平方公里)。
还有一个跨年份比较的坑:不同代际的灯光产品量纲不一样,老一代稳定灯光产品的 DN 是 0 到 63 的整数,新一期的辐射定标产品是连续的辐射值,单位是纳瓦每平方厘米每球面度。两个年份的 SUM 直接放在一起排大小没有意义,必须先做归一化(比如除以当年区域内的最大值),或者做对数变换,再比较相对变化。这一点在写报告时一定要交代清楚口径,否则读者会以为某地灯光翻了一倍。
4.3 连接回矢量:字段类型对不上,全是 Null
统计表是独立的,要挂到行政区图层上才能做符号化和出图。用右键图层 → 连接和关联 → 连接,连接字段选矢量里的行政区代码,表中字段选统计表里的ADCODE(区域字段那一列),连接后统计字段会以表名.SUM的形式出现在属性表末尾。
连接失败的三种典型表现和对策:
第一种,连上之后所有统计字段是<Null>。原因几乎都是两边字段类型不一致,一边是文本型的“110101”,一边是数值型的 110101。解决办法是统一成同一类型,通常把矢量那边的代码字段转成数值型更稳妥,转之前确认没有前导零被吃掉。
第二种,连接报“字段不唯一”。说明矢量图层里存在重复的行政区代码,或者统计表里有重复行(区域面本身有重叠)。先把重复值查出来处理掉,再连接。
第三种,连上了但值明显对不上号。这种多半是区域字段选错,比如统计时用的是 OBJECTID,连接时拿名称去匹配,编号和名称不是一一对应关系,自然错位。
连接结果建议立刻导出成一个新要素类保存下来,不要只停留在内存里的连接状态。ArcGIS 的图层连接是临时性的,下次打开工程或者换个软件,连接就没了,统计字段跟着消失。
4.4 分级色彩:颜色深浅要经得起追问
符号化在图层属性 → 符号系统里设置。选“数量” → “分级色彩”,字段选刚才连接进来的统计字段,分类方法建议用自然间断点分级法(Jenks),区县数量在十几到几十个时,自然间断能比较好地体现数值的聚集特征;相等间隔适合展示绝对梯度,但遇到极值会把大部分区县压进同一档;分位数每档数量均匀,适合做排名展示而不强调数值差异。
分类数控制在 5 到 7 档。档位太多颜色难分辨,读者看图只能看出“花花绿绿”;档位太少又看不出差异。色带有讲究:灯光强度用单色渐变(浅黄到深橙、浅灰到深蓝都行),不要用彩虹色带,彩虹色的明度不单调,读者会误判高低。
出图走布局视图:插入 → 图名、图例、比例尺、指北针。图例里把字段别名改成“灯光强度(归一化)”,别直接暴露zonal_stat.SUM这种字段名。指北针和比例尺不是装饰,是让图能被独立阅读的必要元素。导出图片时分辨率至少 300 dpi,格式选 PNG 或 TIFF。
5. 避坑排查:五类翻车现场和对应解法
下面这五条都是实际操作里撞过的,按“现象 → 原因 → 解决”写,遇到问题可以直接对号入座。
5.1 掩膜提取后像元值整体变大或出现小数
现象:掩膜结果的最大值超过原始栅格最大值,或者属性里的像元值出现小数,而原始数据全是整数。
原因:输出像元大小和原始栅格不一致,工具自动做了重采样。常见触发点是环境设置里 cellSize 留空,或者掩膜要素的范围比栅格大,工具按掩膜范围重新推算网格。
解决:环境设置里把捕捉栅格设为原始灯光栅格,像元大小直接引用原始栅格,处理范围设为原始栅格范围。跑之前确认一遍,跑完再对比一次最大值。
5.2 分区统计表里大量区域是 0 或者整行缺失
现象:统计表行数比目标区县少,或者某些区的 SUM 是 0。
原因:区域字段选了名称字段且存在同名;或者区域面和栅格实际不重叠(坐标系不同但动态投影让它们看起来叠在一起);或者勾选了忽略 NoData 导致包含 NoData 的区域被跳过。
解决:区域字段改成唯一编号;用“识别”工具在区域内部点一下确认能读到栅格值;忽略 NoData 保持DATA选项,不要选NODATA。
5.3 按属性选择选不中目标区县
现象:SQL 写对了但选择结果为 0 条。
原因:名称字段有前后空格、全角半角混用;字段名大小写和实际不符(shapefile 里可能全大写);字符串引号用了双引号。
解决:先用LIKE '%关键字%'探一下能不能匹配到;能用就在字段计算器里对名称做去空格处理;确认字段名后按 shapefile 规则(字段双引号、值单引号)重写 SQL。
5.4 工具报路径类错误,输出文件生成不出来
现象:执行时报 000210、010240 之类的错误码,或者输出位置找不到文件。
原因:输出路径含中文、空格、特殊字符,或者路径过长;输出格式和个人地理数据库的大小限制冲突。
解决:工作空间和所有中间数据统一放全英文短路径,比如E:/work/;栅格输出用.tif,不要输出到.mdb;跑之前先确认输出目录存在且有写权限。
5.5 连接后统计字段全部为空
现象:连接操作显示成功,但属性表里统计字段全是<Null>。
原因:两边连接字段类型不一致(文本对数值);或者字段值本身有前导零、空格差异;或者统计表里的区域字段值在矢量里根本不存在。
解决:连接前先用“汇总统计”分别看两边的字段值和类型;把类型统一(一般转成数值型);连接完抽查两三行,拿区域代码去统计表里手工比对一次数值,确认没错位再往下做。
6. 进阶用法:把单区流程改成批量和可复核
单次跑一个区县,图形界面点点就够了。实际项目往往是十几个区县乘上好几个年份,手点容易漏、容易串行号,这时候该上模型构建器或者脚本了。
批量思路很简单:把目标区县列表读进来,循环执行“按属性选择 → 导出 → 掩膜提取 → 分区统计”,每次输出文件名带上区县代码和年份。用 arcpy 写的话,主体就是一个for循环套四个函数,环境设置放在循环外只设一次。文件名规范建议定成nl_区县代码_年份.tif,后面合并面板数据时直接按文件名拆字段,不用再手工对表。
# -*- coding: utf-8 -*- import arcpy, os from arcpy.sa import ExtractByMask, ZonalStatisticsAsTable arcpy.CheckOutExtension("Spatial") base = r"E:/work" light_dir = r"E:/nightlight" years = ["2018", "2020", "2022"] counties = {"A县": "1401", "B区": "1402", "C区": "1403"} for year in years: light = os.path.join(light_dir, f"NL_{year}.tif") arcpy.env.snapRaster = light arcpy.env.cellSize = light for name, code in counties.items(): zone = os.path.join(base, f"zone_{code}.shp") # 事先按代码导好的单区面 out_r = os.path.join(base, f"nl_{code}_{year}.tif") out_t = os.path.join(base, f"stat_{code}_{year}.dbf") ExtractByMask(light, zone).save(out_r) ZonalStatisticsAsTable(zone, "ADCODE", out_r, out_t, ignore_nodata="DATA", statistics_type="ALL")循环里每次重新设snapRaster和cellSize,是因为不同年份的栅格可能来自不同产品,分辨率和网格原点未必一致,用上一年的基准去套下一年的数据,等于主动引入重采样。这个细节在脚本里不写出来,跑完一堆年份才发现数值对不上,回头查很痛苦。
批量跑完之后一定要做复核,不能全信脚本。我最常用的复核办法是抽样反算:随机挑三个区县,回到原始整幅栅格上,用同一套掩膜面和统计参数单独跑一遍,和批量结果对比 SUM 和 MEAN。两者应该完全一致,只要有一个对不上,就说明循环里某个环境变量被上一轮污染了。另一个办法是对统计表做分布检查,把所有区县的 MEAN 画个直方图,如果出现明显离群的极高值,多半是某个区县的掩膜面把邻区框进来了。
多期数据合并成面板表时,字段命名统一成SUM_2018、SUM_2020这种带年份后缀的形式,连接时一次只连一张表,连完立即导出,避免多表连接叠加出错。合并后拿两年做差值,得到的“灯光增量”字段比单年绝对值更有分析价值,也更容易发现数据本身的问题——比如某个区两年差值大得离谱,通常不是经济突变,而是某一年的掩膜或统计出了岔子。
从那以后我每次跑完分区统计,都会强制走一遍“抽三个区县手工复核 + 最大值对比原始栅格”这两步,宁可多花二十分钟,也不想在一张错了的图上写结论。希望帮到你。
本文还有配套的精品资源,点击获取