news 2026/10/8 3:44:07

2019年云南10米土地覆盖数据全解析:从栅格读取到面积统计与转移矩阵实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2019年云南10米土地覆盖数据全解析:从栅格读取到面积统计与转移矩阵实战

简介:2019年10m精度云南省土地覆盖土地利用数据,源于基于哨兵影像与深度学习生成的全球陆地覆盖产品,经重新投影为WGS84地理坐标系,并按照最新省市级行政边界裁剪,形成可直接使用的云南省栅格数据集。面向GIS与遥感分析人员、国土空间规划研究者,提供耕地、林地、草地、灌木、湿地、水体、不透水面、裸地、冰雪等十类地表覆盖信息,免去自行下载、拼接、投影转换与分幅裁剪的繁琐环节。资源包共112个文件,压缩后约115.71MB,主要包含tif栅格数据、tfw地理配准文件、xml元数据、dbf属性表、cpg编码文件、png快速预览图及xlsx分类说明,结构清晰,便于直接加载、属性查询与面积统计。全省各州市均单独成图,数据坐标信息完整,可在ArcGIS、QGIS等主流平台中无缝使用。已有327人学习,适合用于土地利用变化监测、生态环境评估、国土空间规划及科研教学等场景。

1. 2019年云南10米土地覆盖数据包:先搞清这是什么精度和值不值得下

2019年10m精度的云南省土地覆盖土地利用数据,这个压缩包我拆过不止一次,里面主体是一张十米分辨率的分类栅格,把云南的林地、灌丛、水体、农田、房屋、裸地都按统一编码切好。很多人看到“10m”第一反应是“比30m清楚一点”,但真正用来算面积、做转移矩阵时才体会到,这个精度能把山地破碎地块的边界画得准不少,前提是你先弄懂它的坐标系和分类体系。这份数据适合做县域国土空间底图、生态保护评估、耕地非粮化监测,或者作为训练样本的标签。它不适合只想知道“云南森林覆盖率多少”这种宏观问题,因为那样的数字用30m数据加统计年鉴就够了。真正值得下载的人,是要把栅格裁剪成研究区、按类别统计出可信面积,甚至和另一年数据做变化分析的从业者。

2. 数据包里到底有什么:分类体系、坐标系与文件清单的拆解

2.1 分类体系:从全球产品到云南区域裁剪的取舍

要判断这份数据能不能用于你的场景,第一步不是找软件打开,而是弄清分类体系。市面上流通的2019年10米云南土地覆盖栅格,绝大多数源自基于Sentinel-2影像的全球级公开土地覆盖产品,比如ESA的WorldCover系列、Esri的全球土地覆盖产品,生产时用随机森林或深度模型对逐像元分类,再按云南行政区边界裁剪。省级裁剪的好处是省去你自己下载全球数据和做掩膜的时间,坏处是裁剪过程可能丢失原始元数据,导致你拿到手的分类代码没有对应的中文图例。

云南的地理特征决定了分类难点集中在“林地”和“灌丛”的区分上。高山峡谷区大量存在稀疏林地、灌丛、草甸的过渡带,全球训练样本在平坦区域表现好,到了云南就容易把一个山坡分成上半截林地、下半截灌丛。所以你拿到数据后,首先要做的是把栅格的所有唯一值读出来,列一张清单。举个例子,常见编码可能如下表所示,但不同来源的数字可能完全不同,以下只是示意:

分类代码中文名称说明
1乔木林地郁闭度较高
2灌木林地包括天然灌丛
3草地含高山草甸
4耕地含水田和旱地
5建设用地房屋、道路、工矿
6水体河流、湖泊、水库
7裸地裸土、裸岩、沙地

我一般建议在QGIS里打开后,先用工具栏的“识别”功能点几个典型位置,比如滇池、昆明城区、哀牢山森林,确认编码和地物是否对得上。如果包里的README缺失,那就要自己通过高分辨率影像比对来推断每个数字的含义。这一步不做,后面的面积统计都是黑匣子,出了错你都不知道往哪找。

2.2 坐标系与投影:为什么打开后形状有点“歪”

原始分类栅格一般默认采用WGS84经纬度坐标系,EPSG编码是4326。像元尺寸在参数中通常写作0.00009度左右,但这并不意味每个像元都是10米乘10米。在赤道附近,0.00009度经度对应的地面距离约10米,但在云南北纬21度到29度区域,经度方向的真实距离要乘以纬度的余弦值。以北纬25度计算,cos25°约等于0.906,也就是说经度方向一个像元实际只有约9.06米,而纬度方向仍是约11米左右,导致单像元面积从“标称的100平方米”变成了实际约99.7平方米,看起来差别不大,但全省数亿个像元累计后,面积误差能达到几千平方公里。

打开后云南的轮廓看起来有点“歪”也是因为经纬度网格在低纬地区并不等距,屏幕显示时默认做了等距投影,所以弧线和直线都会变形。计算面积和做统计之前,必须先重投影到等积投影或UTM投影。常用的方案如下:

投影EPSG特点适用场景
WGS84 经纬度4326原始格式,像元不等面积查看、拼接
UTM 47N32647六度分带,局部变形小单县或局部精确量算
Albers 等积投影视参数而定面积守恒,适合全区域汇总全省统计、制图

需要特别提醒的是,如果你用Albers投影,参数里的中央经线和标准纬线不同,输出像元大小也会不一样。最稳妥的做法是直接在GDAL的Warp里指定目标投影,然后设置输出分辨率,这样后续每一个像元面积都是你设定的值,例如100平方米。

2.3 文件清单:从tif到辅助文件,哪些必须保留

解压这个rar包后,里面通常不会只有一个孤零零的tif。一个合格的成果包至少包含这几个文件:主栅格文件、坐标配准文件、元数据文件和样式文件。主栅格往往是单波段UInt8类型,因为分类数最多不超过几十个,一个字节足够,文件体积一般在几百MB到几个GB之间,云南全省10米分辨率大约有十亿量级像元,压缩成LZW或DEFLATE后能小很多。

坐标配准文件(.tfw)非常关键,它记录了栅格左上角坐标、像元尺寸以及旋转参数。如果没有它,很多软件会把tif当成无坐标文件,无法套到正确位置。元数据文件(.xml或.json)包含数据源、投影、生产日期、分类体系,这是解释数据最权威的凭证。样式文件(.qml或.lyr)是给QGIS或ArcGIS用的配色方案,能让你打开时直接看到彩色分类图,而不是灰度或黑屏。

我习惯的做法是解压后先执行一次完整性检查,用GDAL的gdalinfo命令看看能否正常读取元数据和波段信息。命令示例如下:

gdalinfo LC_2019_Yunnan.tif

输出里重点看Coordinate System、Metadata、Band 1的NoData Value和Minimum/Maximum。如果Coordinate System显示为空,说明坐标文件丢失,需要从同源数据中找回;如果NoData Value没有定义,后面统计面积时就会把背景值当成真实类别,这是一个极其隐蔽的坑。还有一点容易被忽略:不要只保留tif文件,.tfw文件只有几KB,一旦丢了,栅格就变成一张“无定位图片”,等于白下载。

3. 把数据用起来:QGIS和Python读取、裁剪、统计面积的完整流程

3.1 QGIS中快速查看和符号化

拿到数据第一件事,不要急着做统计,先在QGIS里加载看一眼睛。很多人在这一步就开始翻车:加载后整幅图像要么全黑,要么灰蒙蒙。根源在于QGIS默认用连续渐变色带渲染,而分类栅格的值是离散类别代码,比如1、2、3,它们之间没有物理上的连续关系,默认的拉伸渲染会把它们当成灰度值,显示出来自然不是你要的样子。

正确操作是:在“图层”面板右键当前图层,选择“属性”,进入“符号化”选项卡,把渲染类型从“单波段灰度”切换为“单值”。然后点击“分类”按钮,QGIS会自动列出所有唯一值。接着你需要根据元数据里的分类表手动修改每个类的颜色和标签。如果包内含.qml样式文件,直接点击样式下拉框里的“加载样式”,选择该qml文件,QGIS会自动完成全部渲染配置。

这里有一个关键点:建议把背景值设为透明。在单值渲染的列表中,找到代表NoData或背景的类别(常见的是0或255),把填充色设置为“透明”。如果你不透明地显示背景,整幅图的边缘会出现一个巨大的黑色或白色方块,干扰你对有效范围的判断,也影响后续的目视检查。

3.2 Python+GDAL读取和重投影

图形界面适合快速查看,批量处理还是得靠脚本。Python环境下最常用的是GDAL的osgeo模块或Rasterio。下面这段代码演示读取一张10米土地覆盖tif,输出基本信息和唯一值列表:

from osgeo import gdal import numpy as np ds = gdal.Open('LC_2019_Yunnan.tif') band = ds.GetRasterBand(1) print('投影:', ds.GetProjection()) print('仿射变换:', ds.GetGeoTransform()) print('数据类型:', gdal.GetDataTypeName(band.DataType)) print('栅格尺寸:', ds.RasterXSize, 'x', ds.RasterYSize) data = band.ReadAsArray() unique_vals, counts = np.unique(data, return_counts=True) for val, cnt in zip(unique_vals, counts): print(f'类别 {val}: {cnt} 个像元')

逻辑说明:GetGeoTransform()返回六个参数,顺序是左上角x坐标、像元宽度、旋转项、左上角y坐标、旋转项、像元高度。这里旋转项通常为0,像元高度为负值,代表栅格从左上角开始逐行向下。ReadAsArray()默认读入全图,如果文件有好几个GB,内存会非常吃紧,建议改成band.ReadAsArray(col_offset, row_offset, col_count, row_count)分块读取,或者用gdal.Warp先生成一块缩略图。np.unique能快速暴露数据中是否存在意外值,比如某类的代码是128而不是0~10之间,那说明数据可能被错误转换过。

重投影是面积统计前必须做的事。推荐用gdal.Warp:

out_tif = 'LC_2019_Yunnan_UTM47.tif' gdal.Warp(out_tif, ds, dstSRS='EPSG:32647', resampleAlg='near', xRes=10, yRes=10, format='GTiff') print('重投影完成')

参数说明:dstSRS='EPSG:32647'把坐标系直接换成UTM 47N;resampleAlg='near'表示最近邻重采样,这一点必须写死。分类栅格的值是标号,不是连续值,如果使用bilinear或cubic重采样,类别数值会被插值成小数,比如3.7,这既不是草地也不是水体,整个栅格就作废了。xRes=10, yRes=10强制输出像元尺寸为10米,这样后续面积计算可以直接用100平方米乘像素数,不必再动态读取仿射参数。

3.3 按行政区裁剪:从省到县的矢量掩膜提取

如果研究范围是某个县或多边形区域,需要从全省tif中裁剪出子集。常见做法是使用gdal.Warp的cutline参数,配合一个边界矢量文件。下面这段代码把云南省A县边界裁剪到土地覆盖图层上:

gdal.Warp( 'A县_LC_2019.tif', 'LC_2019_Yunnan_UTM47.tif', cutlineDSName='A县边界.shp', cropToCutline=True, dstNodata=255, format='GTiff' )

逻辑说明:cutlineDSName指定矢量边界的路径,cropToCutline=True表示输出范围裁剪到矢量边界的外接矩形,并对边界外的部分填充NoData。但请注意,gdal.Warp默认裁剪的是外接矩形,而不是严格按矢量边界形状。如果矢量边界是凹多边形,外部但仍然在外接矩形内的区域会被填充NoData,算面积时你需要排除255。

如果你需要精确到矢量边界内部,更合适的是用Rasterio的mask函数,它支持像素级掩膜:

import rasterio from rasterio.mask import mask import json with rasterio.open('LC_2019_Yunnan_UTM47.tif') as src: with open('A县边界.geojson') as f: geojson = json.load(f) out_image, out_transform = mask(src, geojson['features'], crop=True, nodata=255) out_meta = src.meta.copy() out_meta.update({ 'height': out_image.shape[1], 'width': out_image.shape[2], 'transform': out_transform, 'nodata': 255 }) with rasterio.open('A县_LC_2019_precise.tif', 'w', **out_meta) as dst: dst.write(out_image)

参数说明:mask函数读入栅格和GeoJSON要素,crop=True让输出范围收缩到要素边界内。注意,掩膜操作会把矢量边界外的像元设置为nodata=255,但边界内的NoData也会被保留为255,后续统计时统跳过即可。这种方式比gdal.Warp更精确,代价是内存占用更高,大区域操作时尽量分块或先做好矢量简化。

3.4 像元面积统计:不要直接用像素数乘100

统计各类别面积是使用这份数据最核心也是最容易出错的环节。先重投影到UTM或Albers,再用以下代码统计:

from osgeo import gdal import numpy as np ds = gdal.Open('LC_2019_Yunnan_UTM47.tif') band = ds.GetRasterBand(1) data = band.ReadAsArray() # 读取像元实际尺寸 gt = ds.GetGeoTransform() pixel_width = abs(gt[1]) pixel_height = abs(gt[5]) pixel_area = pixel_width * pixel_height # 统计唯一值及其出现次数 classes, counts = np.unique(data, return_counts=True) results = [] for cls, cnt in zip(classes, counts): if cls == 255: # 跳过NoData continue area_m2 = cnt * pixel_area area_km2 = area_m2 / 1e6 results.append((cls, area_km2)) print(f'类别 {cls}: {area_km2:.2f} km²')

逻辑说明:gt[1]是经向像元尺寸,gt[5]是纬向像元尺寸,在UTM投影下两者通常都是10米,但你不能假设一定是10,因为重投影时四舍五入可能导致9.999或10.001。最稳妥的方式就是这里动态读取。np.unique返回两个数组,顺序一致,用zip配对即可。注意我把NoData值定为255,如果某个有效类别恰好也是255,就得改成其他值,这是为什么在上一步重投影时设置dstNodata=255之前先检查原始分类最大值的道理。

如果想输出成CSV,加一个np.savetxt或pandas即可。这里建议用pandas:

import pandas as pd from osgeo import gdal import numpy as np ds = gdal.Open('LC_2019_Yunnan_UTM47.tif') data = ds.GetRasterBand(1).ReadAsArray() gt = ds.GetGeoTransform() pixel_area = abs(gt[1]) * abs(gt[5]) classes, counts = np.unique(data, return_counts=True) df = pd.DataFrame({ 'class': classes, 'pixel_count': counts, 'area_km2': counts * pixel_area / 1e6 }) df = df[df['class'] != 255] df.to_csv('Yunnan_LC2019_area.csv', index=False) print(df)

这样处理之后,输出的面积表格可以直接和统计年鉴对照。需要说明的是,栅格分类天然存在混合像元,10米分辨率下梯田边界、道路两侧的细碎地物仍然会被错分,所以面积统计不是物理实测,而是一个基于最大似然分类的估算值,使用在趋势分析上完全没问题。

4. 土地利用转移矩阵:从静态分类到动态变化分析

4.1 转移矩阵的计算逻辑

这份数据本身是2019年单时期的,但做土地利用变化的同行通常会把2019年作为基准期,再找一期相邻年份的数据构建转移矩阵。转移矩阵的基本思想是统计每个像元在初期和末期的类别组合,并把结果填入二维交叉表。矩阵的行表示初期的类别,列表示末期的类别,对角线上的数值表示“未变化面积”,非对角线上的数值表示“从某种类型转变为另一种类型”的面积。

举个例子,假设初期只有草地(3)和耕地(4),末期也只有这两类。矩阵中第一行第一列是“草地转为草地”的面积,第一行第二列是“草地转为耕地”的面积。矩阵中最关键的一步是保证两期栅格的像元一一对应。要求坐标系、分辨率、裁剪范围完全一致,否则一个像元对不上另一个像元的状况,矩阵里会出现大量虚假变化。

由于原始数据已经是2019年,你需要准备第二期。常见做法是下载同源产品的2020年或2021年图幅,再按同一个裁剪范围处理。如果直接拿不同来源的两期数据做矩阵,分类代码不一致会让矩阵无法解释,必须先重分类统一编码。这一步比矩阵计算本身更耗时,但也更重要。

4.2 用Python构建2019年分类的转移矩阵

当两期数据准备好后,下面的代码构建一个最直观的转移矩阵。为方便理解,这里使用一个循环实现,虽然效率不是最高,但逻辑清晰,适合小面积试点。

from osgeo import gdal import numpy as np def read_band(path): ds = gdal.Open(path) band = ds.GetRasterBand(1) return band.ReadAsArray() data_2019 = read_band('LC_2019_Yunnan_UTM47.tif') data_2020 = read_band('LC_2020_Yunnan_UTM47.tif') # 保证两期数据形状一致且在相同位置 assert data_2019.shape == data_2020.shape, '两期栅格尺寸不一致' # 构造有效像元掩膜 valid = (data_2019 != 255) & (data_2020 != 255) a = data_2019[valid] b = data_2020[valid] # 收集所有类别 classes = np.unique(np.concatenate([a, b])) index = {cls: i for i, cls in enumerate(classes)} matrix = np.zeros((len(classes), len(classes)), dtype=np.int64) for a_val, b_val in zip(a, b): matrix[index[a_val], index[b_val]] += 1 print('矩阵行:2019年,矩阵列:2020年') for i, cls in enumerate(classes): print(f'{cls}: {matrix[i, :]}')

逻辑说明:valid掩膜同时排除了两期中的NoData。np.concatenate是为了从两期里收集全部类别编号,保证矩阵行列覆盖所有可能值。循环遍历每个有效像元,把组合计数写入矩阵对应位置。dtype=np.int64务必保留,如果云南全省像元大约10亿,累计计数很容易超过int32上限。

对于大型影像,这个循环会非常慢。改进办法是使用np.add.at:

trans = (np.searchsorted(classes, a) * len(classes) + np.searchsorted(classes, b)) matrix_opt = np.zeros((len(classes), len(classes)), dtype=np.int64) np.add.at(matrix_opt, trans, 1)

searchsorted把类别值映射成从0开始的索引,然后通过一维索引展开二维矩阵的位置,np.add.at在扩展位置进行累加。这种方法比Python循环快两个量级,适合全云南处理。

4.3 结果解读与常见误区

转移矩阵得出后,常见的解读错误有两个。第一是把行和列的方向搞反。建议在输出表格的注解里直接写“行是2019年,列是2020年”,并且导出到CSV时保留类别码,不用阿拉伯数字代替类别名称。第二是直接把两期分类图相减,例如data2020 - data2019,这种做法的结果完全无法解释,因为5变成3和3变成5都会得到2,而且类别编号没有数值含义,减法得到的数字根本不是物理量。

如果要让矩阵变成面积,把每个计数乘以像元面积后放在矩阵里。计算面积时用3.4节的动态像元面积,而不是手动假定100。做完后可以输出一个面积转移表,再用热力图展示哪种转换最频繁。我在实际项目中见过一个常见陷阱:周边省份的边界像元因为云掩膜存在导致NoData不重合,矩阵里出现大量“有到无”的变化,这时候必须先把两期的云掩膜并集作为有效区,再计算矩阵,否则数十万个虚假变化会让结果失真。

5. 避坑记录:10米土地覆盖数据的五个典型坑和一个后悔药

这一章写我和同行踩过的五个具体坑,按“现象→原因→解决”的顺序列出来,后面还有一个后悔药,希望你能绕过这些地方。

5.1 黑屏和花屏:渲染方式不对

现象:加载tif后画面全黑,或者只有少数几个颜色,完全看不出地形和地物的边界。

原因:分类栅格的值是离散代码,不是连续灰度,QGIS默认使用彩色拉伸渲染,把分类数字当成灰度值或连续色带显示,导致没有过渡色的区域表现为黑色。

解决:在图层属性“符号化”中选择“单值”渲染,点击“分类”让软件自动枚举值。如果有.qml样式文件,直接加载样式。如果还是不行,检查数据是否损坏,用gdalinfo的Minimum=... Maximum=...看是否能读取有效范围,如果读取失败,大概率栅格文件本身出了问题。

5.2 面积统计结果偏小5%~10%

现象:用原始tif统计云南林地面积,和《云南省统计年鉴》或三调数据对比,总是少一块,差距有时接近十分之一。

原因:原始WGS84坐标下,像元在经度方向的实际长度随纬度升高而变短。云南大部分处于北纬21°~29°,按北纬25°计算,经度方向一个像元大约只能对应9.05米而不是10米,纬度方向约11.04米,实际像元面积约100平方米但分布不均衡。如果你直接用0.00009度换算成10米,误差就出来了。

解决:面积统计前强制重投影至UTM 47N或Albers等积投影,并设置输出像元尺寸为10米。用gdal.Warp加xRes=10, yRes=10,再用动态读取仿射参数计算面积。从那以后我看到任何一张土地覆盖tif,第一件事就是看坐标系,如果显示EPSG:4326,我绝不会直接用像元数乘以100。

5.3 NoData被当成有效类别

现象:类别统计时出现一个面积几万平方公里的“类别0”,占掉总量一半,整个统计表失去意义。

原因:数据生产时把无效区域或云掩膜设置为0,但很多软件不会自动识别0为NoData。有些数据集则把NoData设为255,你在原始WGS84坐标下可能没注意。

解决:在QGIS里用“标识”工具点击影像外框,看该区域的值是什么,然后统一重编码。最好在重投影时用dstNodata=255,并在后续统计代码里显式跳过255。如果原始NoData就是0,则跳过0。千万别盲目跟网上教程默认“0是背景”,一定要自己确认。

5.4 不同来源数据图例混用

现象:把两个不同年份或不同来源的tif放在一起,发现同一地点类别变化频繁,很多区域在“林地”和“灌木”之间来回跳变。

原因:不同产品分类体系不同。比如某个产品把“疏林”归为林地,另一个产品把它归为灌木,编码更是天差地别。直接用第二套图例去解释第一套数据,必然混乱。

解决:使用前先分别读取唯一值,做成两张“原始编码→统一分类”的重分类映射表。在Python中用np.copy做重编码,例如:

import numpy as np def recode(data, mapping): out = data.copy() for old, new in mapping.items(): out[data == old] = new return out mapping_2019 = {1: 10, 2: 20, 3: 30, 4: 40, 5: 50} mapping_2020 = {2: 10, 3: 20, 5: 30, 7: 40, 9: 50} data_2019_recoded = recode(data_2019, mapping_2019) data_2020_recoded = recode(data_2020, mapping_2020)

这里把两期类别统一到同一套编号体系,这样才能保证转移矩阵有实际意义。这个步骤最费时间,但它是变化分析可信度的根基。

5.5 裁剪后边界出现锯齿和杂斑

现象:用矢量边界裁剪后,边界地带出现很多细碎的类别突变,仔细观察会发现某些像元在边界外但被填入了类别值。

原因:裁剪本质是按像元对齐,矢量边界穿过像元时,该像元会被判定为“部分落入”。gdal.Warp的cropToCutline默认外接矩形填充,如果使用-cutline但未-crop_to_cutline,输出不裁剪;如果用cropToCutline=True,边界外的像元处理取决于重采样方法,有时会产生边缘噪点。

解决:对于边界问题,接受栅格本身的锯齿。不要试图把栅格矢量化成光滑多边形,那会丢失像元精度。如果要做精确边界统计,先用Rasteriomask进行像素级掩膜,再统计,不要在边缘像元上反复纠结。对于杂斑,如果某类面积占比小于0.1%且形态上不符合地物分布,可以用多数滤波或形态学开闭运算清理,但必须评估是否影响真实细小地物。

5.6 后悔药:及时备份原始rar包

现象:裁剪或重投影过程中误覆盖原始tif,想重置某个区域的分类时,只能重新下载整个压缩包,耗时几天。

原因:很多人解压后为了节省空间,删掉了rar包,或者直接在工作目录里把原始tif作为输出文件名覆盖。

解决:下载后先创建一个“原始备份”目录,把rar包复制进去,并设置文件夹只读。工作目录放一份解压副本,所有脚本处理都指向工作副本。我习惯在脚本开头强制检查原始文件是否存在:

import os raw_path = '原始备份/LC_2019_Yunnan.tif' assert os.path.exists(raw_path), '请先检查原始数据是否存在'

这个后悔药听起来太基础,但我在实际中已经看到不止一个同事因为误删原始文件,加班重新下载数据。别让最基础的事成为最大的坑。

6. 进阶验证:用随机样本点检验数据精度,把“10米”变成可信数字

无论这份分类数据来自哪个公开产品,拿到手后最好自己做一轮精度验证,尤其是你要把它作为成果交付时。最常用的方法是生成随机检验点,用高分辨率影像逐点判读真实类别,再与栅格提取值比较,计算整体精度和Kappa系数。十米精度听起来可信,但实际分类结果在云南山区往往比平原地区虚高,验证一下才能给你兜底。

在QGIS中生成本地随机点,按类别分层抽样。每个类别至少抽80个点,样本太少,混淆矩阵会空行或空列,Kappa值也会失真。导出CSV后,用sklearn计算混淆矩阵:

import pandas as pd from sklearn.metrics import confusion_matrix, accuracy_score, cohen_kappa_score df = pd.read_csv('validation_samples.csv') cm = confusion_matrix(df['reference'], df['predicted'], labels=sorted(df['reference'].unique())) oa = accuracy_score(df['reference'], df['predicted']) kappa = cohen_kappa_score(df['reference'], df['predicted']) print('混淆矩阵:\n', cm) print(f'总体精度: {oa:.3f}, Kappa: {kappa:.3f}')

参数说明:reference列是人工判读的类别,predicted列是栅格上提取的类别。labels参数让矩阵行列覆盖所有可能类别,避免某类缺失导致列数不足。cohen_kappa_score会计算Kappa系数,一般Kappa大于0.75说明可用于业务制图,0.6到0.75之间可用于宏观趋势,小于0.6就要谨慎了。

验证结果出来后,查看哪一类最容易混淆。云南最常见的混淆是“草地”和“灌木”,以及高海拔裸岩和建筑。如果你发现某一类的用户精度特别低,可以在后续统计中把它并进更粗略的一级类别,例如把“灌丛”和“草地”合并成“草灌地”。这样损失一点细节,但换来更可信的数字。

那一次我在云南某项目里帮客户统计不同坡度带的林地面积,原始数据Kappa只有0.58,在我合并了草灌类后提升到0.74,客户认账了。从此以后我每次拿到新的10米土地覆盖数据,都会先抽出两三百个点验证一轮,再决定是否用于业务。这个习惯帮我避开了很多“看起来精度高,然并卵”的公开数据。希望这份拆解能帮你把10米数据用到实处,也希望你别跳过验证这一步。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 3:43:30

DSec:面向DeepSeek智能体训练的弹性计算沙箱基础设施

大型语言模型的智能体训练,往往不是模型训练本身有多难,而是“一批智能体同时跑起来”这件事,能把一个普通开发机折腾到怀疑人生。我在把多个DeepSeek驱动的智能体放出去做环境交互、工具调用和多轮自我博弈时,第一波遇到的就是资…

作者头像 李华
网站建设 2026/10/8 3:42:31

pstack-claude:本地可信AI编程助手的进程级实现原理

1. 项目概述:pstack-claude 是什么,它解决的是哪类真实开发痛点?pstack-claude 这个名字乍看像一个工具组合词,但拆开来看,“pstack”是 Linux 系统中一个真实存在的诊断命令,用于打印指定进程的调用栈&…

作者头像 李华
网站建设 2026/10/8 3:42:02

Agent与LLM工程实践:从Tool到Skill的架构演进与安全加固

最近社区里关于 Agent 和 LLM 的讨论密度明显又上了一个台阶,尤其是"Agent 到底是什么""Skill 和 Tool 有什么区别""Harness 是干什么的"这类基础问题被反复问起。说实话,这轮讨论质量比前几个月高不少,至少大…

作者头像 李华
网站建设 2026/10/8 3:41:59

二叉树的右视图:BFS与DFS两种解法详解

1. 这道题到底在问什么:从“站在右边看”到树的层级透视图1.1 题目原意拆解:右视图不是“右子树视图”LeetCode hot100 里二叉树题目不少,199题“二叉树的右视图”是其中辨识度很高的一道。简单说,题目给你一棵二叉树,…

作者头像 李华
网站建设 2026/10/8 3:41:32

用Python和Pygame开发吃豆人:地图建模、碰撞检测与幽灵AI实战解析

简介:Pacman经典游戏的Java实现项目,由Andrei与Marius合作完成,面向正在学习Java游戏开发、图形界面编程或基础人工智能算法的学生与开发者,可作为课程设计、期末项目或入门实践的完整参考,帮助解决从零搭建游戏框架与…

作者头像 李华
网站建设 2026/10/8 3:41:04

AI Coding Agent Workflows:从踩坑到拆坑的完整实践指南

如果你最近也在关注 AI coding,那你大概率绕不开“agent”这个词。我花了大半年时间折腾 AI coding agent workflows,也就是怎么让 AI 编程智能体能真正独立地把活干完——读代码、改文件、跑测试、看报错、再改,而不是每句话都要人盯着。今天…

作者头像 李华