ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#配置GDAL不用愁:用NuGet高效管理DLL依赖

C#配置GDAL不用愁:用NuGet高效管理DLL依赖 1. 方法二的核心思路为什么不手动拷贝dll而用NuGet1.1 方法一和方法二到底差在哪很多做C# GIS开发的朋友第一次碰GDAL基本都是被“配置”逼疯的。早期网上流传的教程清一色是让你去GISInternals下载一个几百MB的预编译包把bin目录下几十个dll一股脑儿拷到exe旁边然后在VS里手动添加对OSGeo.GDAL.dll的引用还要自己去设置环境变量。这套流程我当年也踩过坑太多了版本不匹配、VC运行库缺失、32位和64位混着拷、明明文件都在却报“无法加载DLL”……折腾一晚上最常见的结局是代码一行没写全在跟文件较劲。所以后来我基本上都只推荐用NuGet来配置这也是标题里说的“方法二”。用NuGet的好处是你根本不需要关心GDAL的native依赖到底有哪些也不需要手动下载那一堆文件。装完包之后GDAL的C原生dll会按平台自动放到bin目录的对应位置C#端会多出一个GdalConfiguration.cs文件替你把环境初始化好。你要做的只是写代码这才是真正的“省心模式”。1.2 需要准备的工具和版本坑写这篇的前提是你已经装好了VS2017而且创建项目时选择的是“.NET Framework”类别的控制台应用不是“.NET Core”。这是第一个大坑因为GDAL官方的C#绑定是基于.NET Framework的不带.NET Core的支持。你如果选错了项目类型后面安装包、引用dll、调用方法全会出问题而且报错信息还不直观。项目框架建议用.NET Framework 4.6.1以上VS2017自带就能选。GDAL版本方面我自己的项目长期用2.4.4这个版本稳定、中文路径兼容性比老版本好而且NuGet上就这一个来源不需要再纠结。如果你确实想用更新的GDAL 3.x系列那要额外注意PROJ库配套的问题后面常见问题部分我会单独讲。需要准备的东西就三样VS2017、一个测试用的TIF文件以及稳定的NuGet源。测试文件可以自己拿任意软件导出一张带地理坐标的tif比如从ArcGIS导出或者用GDAL命令行转一张都行。没有的话找一张普通jpg然后记得后面只做常规读写测试也可以但最好还是带地理信息的tif这样测投影和地理变换更直观。下载地址我先统一列个表后面配置过程里会反复提到途径地址说明NuGet包管理https://www.nuget.org/packages/GDAL/方法二核心自动处理依赖不用下载GDAL官网https://gdal.org/download.html源码和官方说明GISInternals预编译包http://www.gisinternals.com/release.php方法一可选手动部署时用注意GISInternals的包是给那些不能联网装NuGet的机器用的或者你想自己构建一套绿色部署包。正常情况下走NuGet就够了这也是“方法二”的精髓。2. VS2017里配置GDAL的完整步骤2.1 新建项目并锁定x64平台打开VS2017文件→新建→项目左侧选择“已安装→Visual C#→Windows 桌面”中间选“控制台应用.NET Framework”名称随意比如叫“GdalDemo”。创建完成之后先别急着写代码马上做两件很重要的事。第一件把项目平台改成x64。右键项目→属性→生成选项卡在“平台目标”下拉框里选择“x64”。如果你的VS2017里这个下拉框里面只有“Any CPU”那要先到菜单栏“生成→配置管理器”在“活动解决方案平台”下拉里选择“新建”创建一个名为“x64”的新平台然后再回到项目属性里面选。这一步几乎决定了后面所有事情的成败。GDAL的原生dll是不区分C#层面的Any CPU这种说法的它要么是x86版要么是x64版。如果你的程序集以Any CPU模式跑在64位系统上JIT会把你的进程变成64位进程但你装到输出目录的GDAL native dll却是x86版那必然报BadImageFormatException。很多新手第一关就栽在这里而且这个错有时候是运行时才爆出来非常隐蔽。第二件确认一下项目的目标框架。把项目属性里的“目标框架”设置成4.6.1或更高。我看到过有人用.NET Framework 4.0去装新版GDAL结果NuGet里一大堆包直接标红不兼容。VS2017默认模板给出的4.6.2或者4.7.2其实都可以不用刻意去降低。2.2 NuGet安装GDAL包的正确姿势平台搞定之后在解决方案管理器里右键项目名称选择“管理NuGet程序包”。在“浏览”页签搜索“GDAL”搜索结果里会出现一个包名就叫“GDAL”的条目作者是Tama McGlinn。注意我这里说的不是“GDAL.Core”也不是“GDAL.Native”单独装而是一个叫“GDAL”的汇总包。这个包会替你拉取依赖的原生dll包。选好“GDAL”之后右侧版本号我建议直接选2.4.4。这个版本是目前C#项目里最成熟、最稳妥的版本教程最多遇到的问题基本都能搜到答案。如果你偏要装3.0以上的版本不是不行但初始化方式变化比较大而且包里可能默认不再生成GdalConfiguration.cs需要你自己处理更多细节新手别贪新。点击“安装”之后注意看VS右下角或输出窗口的安装日志。正常情况下会安装两个包GDAL本体以及一个类似“GDAL.Native”的依赖包这个native包才是真正包含gdal.dll、ogr.dll等原生文件的东西。安装完成后解决方案资源管理器里会多出一个文件叫GdalConfiguration.cs如果没有的话说明你装的包不是带自动配置的那一版后面我给出手工初始化的兜底方案。此时去项目的bin/Debug目录里看一眼你会发现多了一堆dll。NuGet包的输出结构一般是这样的项目输出目录下直接有gdal_csharp.dll、ogr_csharp.dll这类C#中间层文件而真正的gdal.dll、ogr.dll等native文件则会在bin目录下或在bin目录下的一个子目录里按运行平台分别放置。不管它放在哪GdalConfiguration.cs会在程序启动时自动把native目录找出来加入PATH你不需要自己去移动。2.3 初始化GDAL环境GdalConfigurationNuGet安装完成后不管你是通过“GDAL”包自动生成的GdalConfiguration.cs还是其他方式得到的初始化代码的写法都大同小异。你只需要在程序的Main函数最前面调用GdalConfiguration.ConfigureGdal()和GdalConfiguration.ConfigureOgr()官方C#封装就会去定位所有native的dll路径并设置好GDAL_DATA和PROJ_LIB等相关环境变量。using System; using OSGeo.GDAL; using OSGeo.OGR; namespace GdalDemo { class Program { static void Main(string[] args) { // 初始化GDAL和OGR环境必须放在任何GDAL调用之前 GdalConfiguration.ConfigureGdal(); GdalConfiguration.ConfigureOgr(); Console.WriteLine(GDAL初始化完成); } } }如果你的项目里没有GdalConfiguration.cs那就需要手动做初始化。最可靠的做法是在Main开头加上这么一段string baseDir AppDomain.CurrentDomain.BaseDirectory; string gdalData System.IO.Path.Combine(baseDir, gdal-data); string projData System.IO.Path.Combine(baseDir, proj); Gdal.SetConfigOption(GDAL_DATA, gdalData); Gdal.SetConfigOption(PROJ_LIB, projData); Gdal.AllRegister(); Ogr.RegisterAll();这里有个很容易忽略的细节GDAL_DATA和PROJ_LIB必须指向真实存在的目录。GDAL需要gdal-data目录里的datum数据和坐标系统定义文件如果这个目录不存在或者指向空目录程序不会马上报错但当你去读投影信息或者做坐标系转换的时候就会蹦出一堆奇怪的“unable to initialize coordinate transformation”错误具体问题我放在最后一部分细说。2.4 手工部署模式下载二进制包该放到哪虽然方法二主张用NuGet但有些公司内网环境确实不允许开发机连NuGet还有的场景是要把写好的程序分发到客户机器上运行。这时候你只能走老路手动下载预编译包然后手动部署。如果是内网开发环境那就在一台能联网的机器上用NuGet安装一次然后把整个bin目录以及packages缓存打包拷进去。但如果是分发到客户机器我建议直接从GISInternals下载对应版本的release包把其中bin目录下所有文件复制到你的exe输出目录。注意必须把gdal-csharp、ogr-csharp这些C#封装层的dll和gdal.dll等原生dll放在同一目录最简单的方式就是全部平铺到exe旁边。手工模式下还要记得带上gdal-data目录这个目录一般要从release包里的data目录拷贝过来。很多人手工部署后报“找不到proj.db”或“无法解析坐标系统”八成就是这个目录没拷全。如果你用的是GDAL 3.x的包还要把proj子目录里的proj.db一并拷过去。GDAL 3.x版本里PROJ库升级到6或7以后很多坐标信息都集成在proj.db这个数据库里少了它就是一堆莫名其妙的错误。3. 代码测试读一幅TIF影像的完整示例3.1 从打开数据到读取影像基本信息环境配好之后重点就是验证能不能正常干活了。我用一段实际测试过的代码来说明这段代码的作用是打开一张tif影像读取它的宽高、波段数量、投影信息、六参数仿射变换然后取出中心点的像素值。using System; using OSGeo.GDAL; namespace GdalDemo { class Program { static void Main(string[] args) { GdalConfiguration.ConfigureGdal(); GdalConfiguration.ConfigureOgr(); string filePath D:\test\dem.tif; ReadRasterInfo(filePath); Console.WriteLine(按任意键退出...); Console.ReadKey(); } static void ReadRasterInfo(string path) { // 注册所有栅格格式驱动 Gdal.AllRegister(); using (Dataset ds Gdal.Open(path, Access.GA_ReadOnly)) { if (ds null) { Console.WriteLine(打开文件失败请检查路径或文件格式); return; } Console.WriteLine(影像宽: {0}, 高: {1}, ds.RasterXSize, ds.RasterYSize); Console.WriteLine(波段数: {0}, ds.RasterCount); // 投影和地理变换信息 string proj ds.GetProjection(); Console.WriteLine(投影信息: {0}, string.IsNullOrEmpty(proj) ? 无 : proj); double[] gt new double[6]; ds.GetGeoTransform(gt); Console.WriteLine(左上角X: {0}, 左上角Y: {1}, gt[0], gt[3]); Console.WriteLine(像素宽度: {0}, 像素高度: {1}, gt[1], gt[5]); Console.WriteLine(旋转系数: {0}, {1}, gt[2], gt[4]); // 遍历所有波段并读取中心点像素 int centerX ds.RasterXSize / 2; int centerY ds.RasterYSize / 2; for (int i 1; i ds.RasterCount; i) { using (Band band ds.GetRasterBand(i)) { double[] minMax new double[2]; band.ComputeRasterMinMax(minMax, 0); Console.WriteLine(波段{0}: 数据类型{1}, 最小值{2}, 最大值{3}, i, band.DataType, minMax[0], minMax[1]); float[] pixel new float[1]; band.ReadRaster(centerX, centerY, 1, 1, pixel, 1, 1, 0, 0); Console.WriteLine(波段{0}中心点像素值: {1}, i, pixel[0]); } } } } } }这里重点说一下GeoTransform数组的六个值。它跟GDAL的坐标换算关系非常直接gt[0]和gt[3]是影像左上角的投影坐标X和Ygt[1]是东西方向每个像素代表的投影单位gt[5]是南北方向每个像素代表的投影单位。如果影像没有地理参考GetGeoTransform返回的值就全是0这很正常不代表代码出错。程序里我用using块包住Dataset和Band是因为C#封装层里有非托管资源虽然Gdal.Dispose也能释放但using更保险。实际跑这段代码时控制台会输出影像宽度高度、波段数、投影WKT字符串、像素分辨率以及每一波段的最小最大值。如果你能看到这些输出说明整条GDAL链路已经完全打通。3.2 创建一幅新栅格文件光会读还不行写文件的能力在很多时候更常用。比如做遥感处理经常要把中间结果输出成新的tif或者从一个大影像里裁剪一块出来。下面这段代码演示怎么创建一个新的GeoTIFF文件并把第一个波段的像素值复制过去。static void CreateNewRasterFromDataset(Dataset src, string outputPath) { Driver driver Gdal.GetDriverByName(GTiff); if (driver null) { Console.WriteLine(找不到GTiff驱动); return; } using (Dataset dst driver.Create(outputPath, src.RasterXSize, src.RasterYSize, src.RasterCount, DataType.GDT_Float32, new string[] { COMPRESSLZW })) { // 复制地理参考信息 dst.SetProjection(src.GetProjection()); double[] gt new double[6]; src.GetGeoTransform(gt); dst.SetGeoTransform(gt); // 逐波段复制数据 for (int i 1; i src.RasterCount; i) { using (Band srcBand src.GetRasterBand(i)) using (Band dstBand dst.GetRasterBand(i)) { int width src.RasterXSize; int height src.RasterYSize; int bufSize width * height; float[] buffer new float[bufSize]; srcBand.ReadRaster(0, 0, width, height, buffer, width, height, 0, 0); dstBand.WriteRaster(0, 0, width, height, buffer, width, height, 0, 0); } } dst.FlushCache(); Console.WriteLine(新影像已生成: outputPath); } }这段代码里有两个地方值得专门讲一讲。第一个是“COMPRESSLZW”选项。GeoTIFF支持多种压缩方式LZW是无损压缩对遥感影像效果不错。你的实际场景如果只要粗略中间结果那也可以不压缩直接把options参数传null。另一个常用选项是“TILEDYES”它会把影像切成块存储适合需要频繁读取局部区域的场景。空间换时间按需选择。第二个是FlushCache()。GDAL在写入栅格数据时有自己的缓存FlushCache把缓存强制写入磁盘相当于确保数据落盘。如果不调用它在正常释放Dataset对象时也会自动刷新但保险起见显式调用一次没有坏处尤其当程序后面还有大量操作时提前落盘可以避免内存占用过高。3.3 用OGR读写矢量数据的准备工作GDAL不是只能处理栅格它的兄弟OGR专门负责矢量数据在NuGet方案里这两个是一起被初始化的。我遇到很多人配好GDAL之后想读shp文件却发现找不到OGR相关入口其实就是初始化时少调了ConfigureOgr或者在代码里没有用Ogr.RegisterAll()。OGR读shp的典型代码结构如下先给你看一眼后面有专门做GIS分析的朋友可以直接参考using OSGeo.OGR; static void ReadShapefile(string path) { DataSource ds Ogr.Open(path, 0); if (ds null) { Console.WriteLine(打开矢量文件失败); return; } Layer layer ds.GetLayerByIndex(0); Console.WriteLine(图层要素数量: {0}, layer.GetFeatureCount(1)); Feature feat; while ((feat layer.GetNextFeature()) ! null) { int fieldCount feat.GetFieldCount(); for (int i 0; i fieldCount; i) { Console.WriteLine(字段{0}: {1}, feat.GetFieldDefnRef(i).GetName(), feat.GetFieldAsString(i)); } break; // 只打印第一条要素 } }OGR和GDAL在初始化环境之后基本就是平趟的唯一要记住的是矢量文件打开方式和栅格不一样shp文件只需要给文件路径OGR会自动扫描同名的shx、dbf等附属文件你千万别自己手动去挨个打开那些附属文件负责读取的驱动会帮你处理。4. 常见问题与排查技巧实录4.1 初始化异常和dll加载失败先说一个我见过无数次的报错“System.TypeInitializationException”或者“OSGeo.GDAL.GdalPINVOKE”的类型初始值设定项引发异常。网上搜这个错误能出来一堆帖子但很多人给出的答案都只说“缺少dll”实际上没这么简单。这个问题最常见的3个原因第一平台目标没设置成x64程序集加载不到正确位数的gdal.dll第二bin目录下根本找不到gdal_x64.dll或gdal.dll这个是因为NuGet没有正确还原或者手动部署时漏拷第三全套dll都在但VC运行库版本太低GDAL原生dll需要的运行库没装上。排查顺序建议这样先看项目平台是不是x64再看输出目录里有没有gdal.dll这一类的原生文件最后用Dependencies之类的工具查一下gdal.dll依赖的VC运行库。很多人卡死在第三步因为VS2017自带的VC运行库不一定覆盖GDAL要求的低版本运行库这时需要给目标机器装一个合适的VC Redistributable包。4.2 平台目标不匹配导致的“暗坑”平台目标这个坑我觉得得单独拿出来说因为它太容易踩了。GDAL的native dll是区分x86和x64的但C#项目默认的平台目标是“Any CPU”。这就导致了一个非常迷惑的现象你在本机调试一切正常但换一台机器就报“未能加载文件或程序集 OSGeo.GDAL”或者“试图加载格式不正确的程序”。更麻烦的是VS2017里的Any CPU在某些环境下会自动偏向64位在另一些环境下又可能偏向32位这个行为取决于目标机器的操作系统和IIS等宿主进程。一旦你把程序部署成32位进程去跑x64版本的GDAL或者反过来那必然会出问题。如果你在NuGet里把GDAL.Native包同时装上了x86和x64两个平台的dll那理论上程序自己会按进程位数去定位但这种自动定位有时候也会失败。我个人的建议是从最开始就强制指定平台目标不要用Any CPU。开发机是64位就锁死x64开发机是32位就锁死x86。锁死之后运行时的行为完全可预测排查问题也方便很多。4.3 GDAL 2.x中文路径问题老版本GDAL对中文路径的支持一直让人头疼。GDAL 1.x和早期的2.x版本在Windows上打开含中文的路径会直接失败返回null。我当年就是因为这个被迫给程序里的所有输入路径做了临时改名处理先把文件拷贝到一个纯英文目录再处理处理完再拷贝回去后来升级到GDAL 2.4.4之后这个问题基本就消失了。如果你还在用老版本而且没法升级有一个折中办法使用Windows短文件名格式。比如路径“D:\测试数据\影像.tif”可以先用kernel32的GetShortPathName转换成类似“D:\TEST~1\影像.tif”的格式再交给GDAL打开。这个方法不算优雅但在生产环境里确实管用。如果你是GDAL 3.x中文路径问题基本不存在了但还是建议所有开发机上的项目文件路径都保持纯英文减少一些第三方库带来的不确定性。4.4 免安装部署GDAL到其他机器开发完成之后总要把程序发给同事或者部署到服务器上。我见过不少人在这一步重蹈覆辙把exe拷过去就跑不起来。原因还是那一套GDAL依赖的native dll不全或者没有对应的VC运行库。最稳妥的打包方式是整个bin目录一起拷走因为NuGet在生成时已经帮你把原生dll放到了正确位置。如果你的程序是手工部署模式带出去的那我建议把gdal-data目录也一起放在exe同级的目录下同时程序初始化时不要写死绝对路径而是动态获取AppDomain.CurrentDomain.BaseDirectory作为基准这样程序挪到哪里都能找到数据目录。部署到服务器上还有一个特殊细节很多服务器是Windows Server Core或者极度精简的Windows环境可能连Visual C运行库都没有。这时候你必须在目标服务器上安装对应版本的VC Redistributable否则GDAL的原生dll会由于缺少依赖直接加载失败。这一条文档里很少提但实战中特别常见。错误现象可能原因解决方案GdalPINVOKE类型初始化异常gdal.dll缺失、平台不匹配、VC运行库缺失按平台放置dll安装VC运行库找不到GdalConfiguration类没安装带配置模板的包手动调用Gdal.SetConfigOption和Gdal.AllRegister读取shp时图层为nullOGR.RegisterAll未调用初始化时调用Ogr.RegisterAll()打开中文路径返回nullGDAL 2.x中文编码问题升级到2.4.4以上或使用短路径投影转换报错unable to initializeGDAL_DATA/proj.db目录不正确设置正确的GDAL_DATA和PROJ_LIB路径BadImageFormatExceptionC#托管层和native层位数不一致显示锁定x64或x86不要使用Any CPU回顾整条链路其实GDAL在C#里真正难的不是API而是那一堆隐藏在背后的native依赖。方法二通过NuGet把这些依赖管理起来已经让整个事简单了不止一个量级。我用VS2017 GDAL 2.4.4这套组合做了好几个项目从DEM读取、影像裁剪到矢量叠加基本没有因为环境问题再回过头来折腾过。最后再分享一个经验新建项目装GDAL时先写一段只读取影像宽高的代码跑通流程再逐步往里面加波段读取和写文件逻辑。这样一旦报错你能快速判断是环境问题还是自己的代码问题排查范围小很多。
返回列表