资讯详情

资讯详情

VS2017预编译GDAL接入与排错:ABI核对到坐标转换

简介面向使用 Visual Studio 2017 进行 GIS 或 C 开发的工程师这份已编译完成的 GDAL 库免去了 CMake 配置、依赖处理与驱动裁剪等繁琐编译环节适合需要快速验证栅格/矢量读写能力、搭建原型或排查环境问题的开发者。压缩包约 103.88MB共 6305 个文件以 obj、cpp、h、c 及 vc 工程文件为主同时包含 lib、dll、exp 等库文件与 html、txt、readme 等说明文档并附带 ogr2ogr、gdalwarp、gdal_translate 等命令行工具及测试数据便于核对版本与功能。已有 2110 人学习下载。包内保留较完整的源码与工程结构可辅助定位编译选项和依赖关系减少重复构建成本也为理解 GDAL 模块组成与后续扩展提供参考。1. 编译好的 GDAL 落在 VS2017 上先搞清楚它绑死了什么上周接手一个老项目交接文档只有一行字编译好的 GDAL仅适用于 VS2017。压缩包解压出来是 include、lib、bin 三个目录看着干净直接加进工程却一路 LNK2019。问题不在 GDAL 本身而在“编译好”这三个字背后藏着三条硬约束工具集是不是 v141、运行库是 /MD 还是 /MT、以及 x86 还是 x64。任意一条对不上这份库就完全用不了。这份资源适合手里有 VS2017 工程、要读栅格影像或做坐标转换、又不想花半天从源码编 GDAL 的人也适合用 VS2019/2022 但愿意装回 v141 工具集的人。下面按拆包核对、接进工程、跑通场景、排错、自检的顺序走一遍每一步都给出可抄的命令和代码。2. 拆包对号工具集、运行库、架构三处必须对上2.1 为什么“编译好”必须绑定编译器版本MSVC 的 C ABI 不跨大版本兼容。v140、v141、v142、v143 各自带一套 CRT 和一套 STL 实现连std::string的内部布局都可能不一样。GDAL 虽然对外主要暴露 C 函数GDALOpenEx、GDALRasterIO这一层但它同时导出了大量 C 类GDALDataset、OGRGeometry、OGRSpatialReference。这些类有虚函数表、有成员布局跨工具集去 new 一个再 delete等于在堆的边界上冒险。所以“仅适用于 VS2017”翻译过来就是三条v141 工具集、某个固定的运行库选项、某个固定架构。第二条约束是运行库。VS 工程里「C/C → 代码生成 → 运行库」有四个可选值/MD、/MDd、/MT、/MTd。GDAL 的二进制包如果是/MD编的你的工程也必须/MD用/MT去链报的错通常不是“运行库不匹配”这么直白而是LNK2038: 检测到“RuntimeLibrary”的不匹配项或者更隐蔽的能链上运行到某个CPLFree就崩。Debug 配置还有第三条_ITERATOR_DEBUG_LEVELDebug 是 2、Release 是 0混用同样是 LNK2038。如果手上是 VS2019 或 VS2022不一定非要重新编 GDAL。VS Installer 的「单个组件」里能找到旧版 MSVC 生成工具装上 v141 之后在工程属性「常规 → 平台工具集」里切回 Visual Studio 2017 (v141)这份包就能直接用。这条路比从源码编 GDAL 划算太多——编 GDAL 要先把 PROJ、SQLite、TIFF、JPEG、PNG、zlib 一串依赖凑齐光依赖版本对齐就够折腾半天。需要注意的是 v141 工具集在新版 VS 里只是“能用”不再更新长期项目还是要规划迁移。判断包里到底是/MD还是/MT不用猜dumpbin能直接读出来:: 读 .drectve 段里的默认库指令 :: MSVCRT / MSVCRTD 出现 /MD 系LIBCMT / LIBCMTD 出现 /MT 系 dumpbin /directives gdal_i.lib | findstr /i MSVCRT LIBCMT这条命令的逻辑是MSVC 在编译每个 obj 时会把运行库选择写进.drectve段最终汇总到 lib 里。如果输出里同时出现MSVCRT和LIBCMT说明这个包是混着编的第三方依赖用了不同选项这种包最容易在运行期出堆相关的怪问题遇到就要谨慎。2.2 三分钟看清包里每个目录的职责别急着配工程先把目录结构过一遍。一份完整的预编译 GDAL 包目录职责大致如下目录典型内容漏掉之后的症状includegdal.h、gdal_priv.h、ogr_*.h、cpl_*.h编译期直接找不到头文件libgdal_i.lib导入库、gdal.lib静态库编译过、链接报 LNK2019bingdal.dll以及proj_*.dll、sqlite3.dll、geos*.dll等依赖双击 exe 闪退错误码 0xc0000135data或shareproj.db、gcs.csv、pcs.csv、各种.xsd能打开影像但坐标转换返回空这里有个顺序问题值得强调先确认这个包本身是完整的再动自己的工程。包里一般会带gdalinfo.exe直接在命令行跑一下:: 先证包再证工程 gdalinfo --version gdalinfo --formats | findstr /i GTiff HFA ENVI如果gdalinfo自己都跑不起来后面所有链接错误都是伪问题先解决 dll 和 data 目录。如果gdalinfo --version正常但gdalinfo your.tif报坐标相关的错那问题一定在data目录跟 lib、include 一点关系都没有。2.3 用 dumpbin 确认架构和导出符号架构对不上是最容易被忽略的一条。x64 工程链了 x86 的 lib报错信息是LNK1112: 模块计算机类型“x86”与目标计算机类型“x64”冲突还算好认但如果链接器目录里同时有 x86 和 x64 两份 lib工程又只写了一个gdal_i.lib那就得靠工具确认到底链进去的是哪一份。:: 看导入库的目标架构x64 会显示 machine (x64)32 位显示 machine (x86) dumpbin /headers lib\gdal_i.lib | findstr /i machine :: 看 dll 的运行期依赖缺哪个第三方 dll 在这里能直接看出来 dumpbin /dependents bin\gdal.dll :: 确认导入库里真的有目标符号排除拿到空壳 lib 的可能 dumpbin /exports bin\gdal.dll | findstr /i GDALOpenEx GDALRasterIO OGRCreateCoordinateTransformation/dependents这条的输出要逐个核对bin目录里必须能找到列出来的每一个 dll或者它们已经在系统目录里。老机器上最常见的坑是某台机器装了别家软件自带的proj_6.dll版本比包里的旧系统按 PATH 顺序先加载了那个结果坐标转换行为诡异。这类问题查起来极其费劲所以后面我会建议把 dll 全部拷到 exe 同级目录绕开 PATH 竞争。3. 接进 VS2017 工程四行属性配置加一段能跑的读取代码3.1 工程属性四项配置与运行库一致性检查真正要改的属性就四组但每一组都有一个对应的“后悔药”做法我一般按下面这样配第一组包含目录和库目录。不建议写死绝对路径在「VC 目录」里新建一个用户宏比如GDAL_ROOT D:\sdk\gdal-vs2017然后包含目录填$(GDAL_ROOT)\include库目录填$(GDAL_ROOT)\lib。换机器、换盘符只改宏一处。第二组链接器附加依赖项填gdal_i.lib。注意 Windows 下 GDAL 的导入库通常叫gdal_i.lib静态库叫gdal.lib具体以包里实际文件名为准别照着别人的博客硬写。第三组运行库必须和包一致见 2.1 的检测结果。第四组调试时让 exe 找到 dll。「调试 → 环境」里填PATH$(GDAL_ROOT)\bin;%PATH%这只在 VS 里 F5 有效。真正发布时把bin下所有 dll 拷到 exe 同级目录——这是最省心的做法比让用户改系统 PATH 靠谱一百倍也顺带解决了上面说的 dll 版本竞争问题。如果手上有十几个工程都要接别一个个点属性页做一个.props属性表在「属性管理器」里添加一次性固化上面四组配置。我吃过这个亏某个工程单独改过运行库没人记得半年后重新编一次全线崩。3.2 一段最小可跑的读取代码配置好之后先别上业务逻辑跑个最小验证打开影像、读尺寸、读地理变换、读一行像素。#include gdal_priv.h #include cpl_conv.h #include cpl_error.h #include vector #include iostream int main(int argc, char** argv) { if (argc 2) return 2; // 进程内只需要注册一次重复调用会拖慢启动 GDALAllRegister(); GDALDataset* ds static_castGDALDataset*( GDALOpenEx(argv[1], GDAL_OF_RASTER | GDAL_OF_READONLY, // 只读打开避免锁文件 nullptr, nullptr, nullptr)); if (ds nullptr) { // 打不开时一定要打错误栈光看返回值什么都看不出来 std::cerr open failed: CPLGetLastErrorMsg() std::endl; return 1; } const int w ds-GetRasterXSize(); const int h ds-GetRasterYSize(); const int bands ds-GetRasterCount(); std::cout w x h bands bands std::endl; // gt[0], gt[3] 是左上角坐标gt[1], gt[5] 是像素尺寸 // gt[2], gt[4] 是旋转项绝大多数影像为 0不为 0 说明是带姿态的影像 double gt[6] {0}; if (ds-GetGeoTransform(gt) CE_None) { std::cout origin gt[0] , gt[3] pixel gt[1] x gt[5] std::endl; } else { std::cout no geotransform, 可能缺 .aux.xml 或投影信息 std::endl; } // 只读第一行验证 RasterIO 的数据通路 std::vectorfloat line(static_castsize_t(w), 0.0f); GDALRasterBand* band ds-GetRasterBand(1); if (band ! nullptr) { CPLErr err band-RasterIO(GF_Read, 0, 0, w, 1, // 源窗口x, y, 宽, 高 line.data(), w, 1, // 目标缓冲与宽高 GDT_Float32, // 目标类型会做自动转换 0, 0); std::cout RasterIO (err CE_None ? ok : fail) std::endl; } GDALClose(ds); // 官方方式是 GDALClose不要 delete return 0; }几个参数值得单独说清楚。GDALOpenEx的第二个参数是打开标志GDAL_OF_RASTER表示只要栅格驱动、GDAL_OF_READONLY表示只读两个一起用能显著减少驱动探测时间。第五个参数是options字符串数组常用的是NUM_THREADSALL_CPUS。GetGeoTransform返回CE_None才算有仿射变换返回CE_Failure不代表打不开只是缺地理参考信息。RasterIO的源窗口和目标缓冲宽高可以不同它内部会做最近邻或双线性重采样由GDALRasterIOExtraArg控制默认最近邻。最后GDALDataset是 C 类但内部用引用计数管理用delete在部分版本上会漏掉 flushGDALClose是唯一稳妥的关法。3.3 GDAL_DATA 与 PROJ_LIB两个必须显式设置的环境变量这两个变量不设代码能编能链跑到坐标转换才炸属于典型的“前面全绿、最后一步死”。GDAL_DATA指向data目录GDAL 在里面找投影 WKT 模板和各种 csvPROJ_LIB指向同一个目录PROJ 在里面找proj.db。GDAL 3 之后坐标转换全部走 PROJproj.db找不到时OGRCreateCoordinateTransformation会直接返回nullptr而不会给你任何提示。稳妥的设法是进程启动时、GDALAllRegister之前把真实环境变量写进去路径从 exe 位置反推不要写死绝对路径#include windows.h #include string void SetupGdalEnv() { wchar_t exePath[MAX_PATH] {0}; GetModuleFileNameW(nullptr, exePath, MAX_PATH); // 取 exe 所在目录再拼上随程序一起发布的 gdal-data 目录 std::wstring dir(exePath); dir dir.substr(0, dir.find_last_of(L\\/)); std::wstring dataDir dir L\\gdal-data; // 两个变量都要设GDAL_DATA 给 GDALPROJ_LIB 给 PROJ // 必须在 GDALAllRegister() 之前调用注册之后再设不生效 SetEnvironmentVariableW(LGDAL_DATA, dataDir.c_str()); SetEnvironmentVariableW(LPROJ_LIB, dataDir.c_str()); }这里有个顺序细节容易翻车环境变量要在GDALAllRegister()之前设。GDAL 内部有个配置缓存驱动注册时会读一次之后再改环境变量部分路径不会重新读。如果确实需要在运行时改用CPLSetConfigOption(GDAL_DATA, 绝对路径)走 GDAL 自己的配置层这条一定能生效但同样建议在注册前调。3.4 CMake 工程怎么接这份包老工程用属性页新一点的工程多半是 CMake。别写find_package(GDAL)——它会去系统里找 GDALConfig.cmake跟这份预编译包没关系。直接指定根目录更干脆cmake_minimum_required(VERSION 3.15) project(gdal_smoke CXX) # 用 cache 变量命令行可以覆盖cmake -DGDAL_ROOTE:/sdk/gdal .. set(GDAL_ROOT D:/sdk/gdal-vs2017 CACHE PATH prebuilt gdal root) add_executable(gdal_smoke main.cpp) target_include_directories(gdal_smoke PRIVATE ${GDAL_ROOT}/include) target_link_directories(gdal_smoke PRIVATE ${GDAL_ROOT}/lib) target_link_libraries(gdal_smoke PRIVATE gdal_i) # 编完自动把 dll 拷到 exe 旁边省掉手动复制这一步 add_custom_command(TARGET gdal_smoke POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${GDAL_ROOT}/bin/gdal.dll $TARGET_FILE_DIR:gdal_smoke)target_link_directories要求 CMake 3.13 以上。POST_BUILD那段只拷贝了主 dll实际项目里用file(GLOB ...)把bin下所有 dll 列出来循环拷更省事如果不做这一步每次改完代码 F5 都提示找不到 dll半天时间就这么耗没了。4. 中文路径与坐标转换两个最容易翻车的日常场景4.1 中文路径别信“设成 NO 就能解决”国内项目里十个文件路径有八个带中文。GDAL 处理路径的规则是GDAL_FILENAME_IS_UTF8配置项为YES时它认为传进来的char*是 UTF-8内部在 Windows 上转成宽字符走_wfopen为NO时按本地编码GBK处理。关键是——这个选项在新版 GDAL 里默认就是YES。网上大量老帖子教人设成NO照着做路径里带非 ASCII 字符时直接打不开而且错误信息还很模糊。正确做法只有一条源文件统一用 UTF-8 保存路径字符串是 UTF-8保持这个选项为默认的YES。如果路径来自 Windows API 或某个只在 Windows 下工作的 UI 框架拿到的是wchar_t*那就先转成 UTF-8#include windows.h #include string // 宽字符 - UTF-8GDAL 的 char* 接口期望的就是这个编码 std::string WideToUtf8(const std::wstring w) { if (w.empty()) return std::string(); int n WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr); std::string s(static_castsize_t(n), \0); WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), s[0], n, nullptr, nullptr); return s; } GDALDataset* OpenRaster(const std::wstring widePath) { // 保持默认的 UTF-8 语义不要设成 NO CPLSetConfigOption(GDAL_FILENAME_IS_UTF8, YES); std::string utf8Path WideToUtf8(widePath); return static_castGDALDataset*( GDALOpenEx(utf8Path.c_str(), GDAL_OF_RASTER | GDAL_OF_READONLY, nullptr, nullptr, nullptr)); }再补一个容易被忽略的点VS 工程属性「C/C → 命令行」加/utf-8等价于同时加/source-charset:utf-8和/execution-charset:utf-8。不加这个开关源文件里的中文字面量在 GBK 代码页的机器上会被编成 GBK 字节流传给 GDAL 就变成乱码路径。这个坑我在两个项目上踩过症状都是“英文路径能开中文路径返回 nullptr”。4.2 坐标转换轴序和 proj.db 是两个独立的问题GDAL 3 之后OGRSpatialReference引入了轴序策略这是最容易翻车的地方。EPSG:4326 的权威轴序是纬度在前、经度在后而绝大多数业务代码习惯传经度, 纬度。不设OAMS_TRADITIONAL_GIS_ORDER转换出来的点会跑到地球另一边去看着像坐标系选错了实际只是 x/y 反了。#include ogr_spatialref.h #include ogr_geometry.h // 经纬度转 3 度带高斯克吕格投影EPSG:4547中央经线 117E bool LonLatToProjected(double lon, double lat, double x, double y) { OGRSpatialReference src; src.importFromEPSG(4326); // 关键一行改回“经度在前”的传统顺序否则 x/y 直接互换 src.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); OGRSpatialReference dst; dst.importFromEPSG(4547); dst.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER); OGRCoordinateTransformation* ct OGRCreateCoordinateTransformation(src, dst); if (ct nullptr) { // 这里返回 nullptr 有两种可能EPSG 代码库里没有或 proj.db 没找到 // 先查 PROJ_LIB 指向的目录下有没有 proj.db再查代码 return false; } int ok ct-Transform(1, lon, lat); OCTDestroyCoordinateTransformation(ct); // 必须手动释放 if (!ok) return false; x lon; y lat; return true; }OGRCreateCoordinateTransformation必须配OCTDestroyCoordinateTransformation它不归 GDAL 的对象池管忘了释放就是纯泄漏循环里调用几次内存就上去了。另外importFromEPSG失败时返回的是OGRERR_UNSUPPORTED_SRS很多人不检查返回值后面一路空指针。判断到底是 EPSG 码不支持还是proj.db缺失最快的办法是在命令行跑一次gdalinfo带投影的影像——如果它也报 SRS 解析失败那 100% 是PROJ_LIB的问题。4.3 驱动注册与按需裁剪GDALAllRegister()会把包里所有驱动全注册一遍几百个驱动冷启动要几十毫秒。桌面端每次开影像都调一次用户能感觉到卡。常见的两种裁剪方式一种是只注册必要的驱动用各驱动自带的注册函数// 只要 GeoTIFF 和 Shapefile 时就别全量注册了 GDALRegister_GTiff(); RegisterOGRShape();另一种是留着GDALAllRegister()但用GDAL_SKIP排除掉肯定用不到的重驱动注意这一步必须在注册之前// 排除掉 NetCDF/HDF 这类体积大、初始化慢的驱动 CPLSetConfigOption(GDAL_SKIP, NetCDF HDF4 HDF5); GDALAllRegister();还有一种情况包里把部分驱动编成了插件单独的 dll不在主 dll 里。这时候除了bin目录要给对还要设GDAL_DRIVER_PATH指向插件目录否则GDALAllRegister之后你会发现可用驱动列表比预期少一大截。判断方法就是跑gdalinfo --formats跟官方完整版的格式列表比一比。5. 排查清单LNK2019、缺 proj.db 与运行期崩溃的五种现场5.1 链接期找不到符号或者运行库不匹配现象编译通过链接报LNK2019: 无法解析的外部符号 __imp_GDALOpenEx被引用的位置在main.obj。原因三种可能。一是附加依赖项里没写gdal_i.lib二是库目录指错了链接器在别的目录里找到同名但不同架构的 lib三是 C/C 里加了extern C缺失——GDAL 的头文件自己带了extern C保护除非你手工前向声明了函数原型。解决先在「链接器 → 命令行」里看最终展开的命令行确认gdal_i.lib的完整路径在不在再dumpbin /exports gdal.dll | findstr GDALOpenEx确认符号确实导出最后检查自己有没有绕过头文件手写声明。现象链接报LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MD_DynamicRelease”不匹配值“MT_StaticRelease”。原因工程运行库选项和 GDAL 包的编译选项不一致obj 里带着运行库标记链接器一比对就报。解决按 2.1 的dumpbin /directives结果把工程「代码生成 → 运行库」改成一致的值。注意 Debug 和 Release 是两套配置很多工程只改了 ReleaseDebug 一编就炸。5.2 运行期第一类坐标转换返回空现象程序跑起来影像能打开、像素能读一到坐标转换就返回nullptr或者转换结果全是0。原因PROJ_LIB没设或者设的目录里没有proj.db。GDAL 3 起坐标转换全部走 PROJproj.db是硬依赖缺了不会报错只是静默失败。解决在代码里把PROJ_LIB打出来用资源管理器确认该目录下确实有proj.db文件不是一个同名目录。发布时把整个data目录随程序一起打进去别只拷 dll。5.3 运行期第二类dll 版本串了现象开发机上跑得好好的拷到测试机就闪退事件查看器里是0xc0000135或者0xc000007b。原因0xc0000135是缺 dll0xc000007b通常是加载了位数不对的 dll。还有一种更隐蔽的目标机器上装了别的软件PATH 里有同名但版本不同的proj_8.dll加载顺序在你控制之外。解决把bin下所有 dll 拷到 exe 同级目录。Windows 的 dll 搜索顺序里exe 所在目录优先于 PATH这样能彻底绕开版本竞争。做完之后用dumpbin /dependents your.exe核对一遍清单。5.4 运行期第三类Debug 能跑Release 崩现象Debug 配置下一切正常切 Release 之后打开影像那一步直接崩或者GDALClose时崩在堆上。原因一般还是运行库或_ITERATOR_DEBUG_LEVEL不一致只是 Debug/Release 各有一套配置改了一个忘了另一个。另一种可能是代码里用new创建的GDALDataset用delete释放跨模块释放堆内存Release 下 CRT 不是同一份直接崩。解决两套配置的运行库都对齐所有 GDAL 对象统一用GDALClose/CPLFree/OCTDestroyCoordinateTransformation释放绝不混用delete和free。5.5 中文路径与只读目录现象路径里带中文GDALOpenEx返回空CPLGetLastErrorMsg()只说文件不存在。原因GDAL_FILENAME_IS_UTF8被设成NO或者源文件编码没统一字面量被编成了 GBK 字节流。解决保持该选项为YES源文件统一 UTF-8 并加/utf-8编译开关路径从 Windows API 拿到时先转 UTF-8。另外注意程序装在Program Files下时GDAL 想在同目录写.aux.xml会被拒绝表现为“能读不能写”把工作目录或GDAL_PAM_PROXY_DIR指到可写位置即可。6. 封装与自检换包前后各跑一遍的两张表GDAL 的 C 风格资源管理在业务代码里散着写迟早出事我一般先包一层 RAII 再说#include memory #include gdal_priv.h // 自定义删除器把 GDALClose 绑到智能指针上 struct DatasetDeleter { void operator()(GDALDataset* d) const { if (d) GDALClose(d); } }; using DatasetPtr std::unique_ptrGDALDataset, DatasetDeleter; // 用法DatasetPtr ds(static_castGDALDataset*(GDALOpenEx(...))); // 任何返回路径上都不用再关心什么时候关异常安全也保住了这层封装的价值不在代码量而在于消除“哪个分支忘了关”的可能性。同一套思路可以推广到OGRCoordinateTransformation删除器调OCTDestroyCoordinateTransformation和GDALDriver之外的所有OGR*对象。换包、升级 VS、换机器之后我固定跑一遍这张表五分钟能省下半天检查项动作通过标准架构dumpbin /headers gdal_i.libmachine 与工程平台一致运行库dumpbin /directives gdal_i.lib只有一类 MSVCRT 或 LIBCMT依赖完整性dumpbin /dependents gdal.dll列出的 dll 都在 bin 里数据目录打开带投影的影像做一次转换不报 SRS 解析失败符号dumpbin /exports gdal.dll用到的函数全部存在中文路径用一个中文名文件跑一遍正常打开且能读像素血泪经验是不要等业务代码写完了才验证包。我现在的习惯是新建工程第一件事就是把 3.2 那段最小代码贴进去跑通跑不通就不往下写。因为一旦业务代码堆上去同一个 LNK2019 你分不清是自己写错还是包不对排查成本翻好几倍。这份编译好的包在取的时候顺手看一眼目录名里的架构和运行库标记再对着第 5 章的五条现场过一遍基本就能一次接上。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →