C# 接入 RasterLite2:从原生 DLL 加载到 Coverage、Section、Tile 验证

ℹ️ 实现范围

本文借鉴一个 。NET 8 WinForms RasterLite2 查看器的实际实现。这个查看器不加载 mod_spatialite.dll,也不调用 RL2_LoadRaster() ;它读取已经存在的 RasterLite2 数据库,通过 GDAL 显示完整 Coverage,并通过 SQL + librasterlite2.dll 解码选中的 Tile。这里关注的是"代码可以完成什么"和"每一步如何验证",不是某个项目目录的复制说明。

1. 这类 C# 查看器可以做什么

1_Rasterlite展示

bash 复制代码
已有 dem.sqlite(Coverage: dem)
          │
          ├─ GDAL:RASTERLITE2:dem.sqlite:dem
          │     └─ gdalinfo → 范围;gdal_translate → GeoTIFF → PNG → Mapsui
          │
          └─ Microsoft.Data.Sqlite:只读 SQL
                └─ dem_tiles + dem_tile_data + idx_dem_tiles_geometry
                      └─ librasterlite2.dll:rl2_raster_decode
                            └─ float 像元 → PNG → Mapsui

两条路径互相补证:GDAL 证明 Coverage 能被 RasterLite2 驱动完整读取;SQL Tile 路径证明应用能按空间范围定位、读取并解码实际的 Tile BLOB。它们都成功时,才可以说"Coverage 和被选中的 Tile 都可读"。

除 RasterLite2 读取外,示例还完成了这些桌面 GIS 功能:

  • 创建 WinForms 地图窗口,使用 Mapsui 显示栅格和矢量图层。
  • 将完整 DEM 转成可显示的 PNG,并按 Coverage 范围放入地图。
  • 按一个选择多边形筛选相交 Tile,只解码需要的局部数据。
  • 将有效浮点像元转成灰度图,并可把像元中心绘制成点图层。
  • 读取 Shapefile 作为行政区、地形地貌、土壤类型等叠加图层。
  • 通过图层树独立开关完整 DEM、局部 Tile、像元中心点和矢量图层。
  • 对数据库缺失、GDAL 子进程失败、Tile 查询为空、native 解码失败等情况抛出明确异常。

⚠️ 不要混淆"读取"和"导入"

当前项目是查看器,不负责创建 Coverage 或把 GeoTIFF 导入 RasterLite2。因此它不能作为 RL2_CreateRasterCoverageRL2_LoadRaster 或 SpatiaLite 扩展加载的运行证据。

2. 实际依赖与运行条件

要实现上述功能,托管依赖可以按下面的最小组合准备:

bash 复制代码
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.10" />
    <PackageReference Include="Mapsui.WindowsForms" Version="5.1.0" />
    <PackageReference Include="Mapsui.Nts" Version="5.1.0" />
  </ItemGroup>
</Project>

运行时还需要下列外部资源;可以放在配置文件、应用目录或命令行参数指定的目录中:

资源 用途 本项目的用法
dem.sqlite 已存在的 RasterLite2 数据库 以只读方式由 Microsoft.Data.Sqlite 打开;GDAL 也从它读取 Coverage
librasterlite2.dll 及其依赖 解码 tile_data_odd / tile_data_even SetDllDirectoryPATH 后由 P/Invoke 装载
gdalinfo.exegdal_translate.exe 读取 Coverage 元数据、导出预览 GeoTIFF 子进程方式调用
RASTERLITE2 driver 的 GDAL 运行时 识别 RASTERLITE2: 数据源 PATH 中包含 GDAL、SpatiaLite、RasterLite2、vcpkg 目录
GDAL_DATA GDAL 数据文件(坐标系等) ProcessStartInfo.Environment 中设置

Microsoft.Data.Sqlite 在这里使用其随 NuGet 提供的 e_sqlite3。它只执行普通的 SELECT,不需要数据库连接拥有 RasterLite2 SQL 函数。因此,不能从这个项目推导出"RL2_Version() 可在 SqliteConnection 上执行"。

3. Coverage:用 GDAL 驱动读取完整 DEM

3.1 RASTERLITE2: 数据源的关键细节

RasterLite2 GDAL driver 用冒号分隔数据库和 Coverage:

bash 复制代码
// 因 ':' 是数据源分隔符,Windows 上使用数据库文件名,
// 并将 GDAL 工作目录设为数据库所在目录。
var source = $"RASTERLITE2:{Path.GetFileName(Database)}:{Coverage}";

不能直接把 Windows 绝对路径拼进数据源,否则盘符中的 : 会破坏数据源解析。正确做法是把 GDAL 工作目录设为数据库所在目录,再传入数据库文件名。

3.2 先读取范围,再生成可显示的 PNG

AddFullDem 的核心流程如下:

bash 复制代码
using var metadata = JsonDocument.Parse(
    RunGdal("gdalinfo.exe", "-json", source));
 
var corners = metadata.RootElement.GetProperty("cornerCoordinates");
var lowerLeft = corners.GetProperty("lowerLeft");
var upperRight = corners.GetProperty("upperRight");
var extent = new MRect(
    lowerLeft[0].GetDouble(), lowerLeft[1].GetDouble(),
    upperRight[0].GetDouble(), upperRight[1].GetDouble());
 
var tifPath = Path.Combine(Path.GetTempPath(), "rasterlite2-full-dem.tif");
RunGdal("gdal_translate.exe",
    "-of", "GTiff", "-ot", "Byte", "-outsize", "1200", "0",
    "-scale", "0", "145", "0", "255", source, tifPath);
 
using var image = Image.FromFile(tifPath);
using var png = new MemoryStream();
image.Save(png, ImageFormat.Png);
var feature = new RasterFeature(new MRaster(png.ToArray(), extent));

这里的 0145 是该 DEM 的显示范围,不是 RasterLite2 的通用值;项目把它们定义为 DemMinimumDemMaximum。实际数据应使用其真实统计范围或显式的渲染策略。

调用外部程序的统一入口还必须同时设置 GDAL_DATA 与动态库搜索路径:

bash 复制代码
private static string RunGdal(string executable, params string[] arguments)
{
    var start = new ProcessStartInfo(Path.Combine(GdalApps, executable))
    {
        WorkingDirectory = Path.GetDirectoryName(Database)!,
        UseShellExecute = false,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        CreateNoWindow = true
    };
    foreach (var argument in arguments) start.ArgumentList.Add(argument);
    start.Environment["GDAL_DATA"] = GdalData;
    start.Environment["PATH"] = string.Join(";", new[]
    {
        GdalApps, GdalBin, SpatiaLiteBin, RasterLiteBin, RasterLiteDirectory,
        VcpkgBin, start.Environment["PATH"]
    });
 
    using var process = Process.Start(start)
        ?? throw new InvalidOperationException("Cannot start GDAL.");
    var stdout = process.StandardOutput.ReadToEnd();
    var stderr = process.StandardError.ReadToEnd();
    process.WaitForExit();
    if (process.ExitCode != 0)
        throw new InvalidOperationException($"{executable} failed: {stderr}");
    return stdout;
}

💡 Coverage 验收

gdalinfo -json RASTERLITE2:dem.sqlite:dem 成功并取得正确范围,说明 GDAL 已找到 RASTERLITE2 driver,且名为 dem 的 Coverage 可读取。gdal_translate 进一步成功则证明该 Coverage 可被完整导出。

4. Tile:按空间范围 SQL 查询实际 BLOB

4.1 查询 Tile 表、数据表和空间索引

项目不用 RL2_Get... SQL 函数,而是查询 RasterLite2 生成的 Coverage 专属表:

bash 复制代码
using var connection = new SqliteConnection(
    $"Data Source={Database};Mode=ReadOnly");
connection.Open();
 
using var command = connection.CreateCommand();
command.CommandText = """
    SELECT r.xmin, r.ymax, d.tile_data_odd, d.tile_data_even
    FROM dem_tiles t
    JOIN dem_tile_data d ON d.tile_id = t.tile_id
    JOIN idx_dem_tiles_geometry r ON r.pkid = t.tile_id
    WHERE t.pyramid_level = 0
      AND r.xmax >= $minX AND r.xmin <= $maxX
      AND r.ymax >= $minY AND r.ymin <= $maxY
    """;
command.Parameters.AddWithValue("$minX", envelope.MinX);
command.Parameters.AddWithValue("$maxX", envelope.MaxX);
command.Parameters.AddWithValue("$minY", envelope.MinY);
command.Parameters.AddWithValue("$maxY", envelope.MaxY);

这条 SQL 选择原始分辨率(pyramid_level = 0)下、与用户选择多边形外包框相交的 Tile。idx_dem_tiles_geometry 提供空间范围,dem_tile_data 提供奇、偶 BLOB。表名来自当前 Coverage dem;换成 Coverage foo 时,应对应检查 foo_tilesfoo_tile_dataidx_foo_tiles_geometry,而不是照抄 dem_*

查询到 0 行时,项目会抛出异常:

bash 复制代码
if (features.Count == 0)
    throw new InvalidOperationException("The SQL query returned no RasterLite2 tiles.");

这是一条有效的 Tile 层验收边界:SQL 能打开库却返回 0 行,只能说明数据库可读,不能说明当前空间范围内有栅格数据。

4.2 解码 tile_data_odd / tile_data_even

在打开 SQLite 连接前,项目先设置 RasterLite2 DLL 及其依赖的搜索路径:

bash 复制代码
NativeKernel32.SetDllDirectory(RasterLiteDirectory);
Environment.SetEnvironmentVariable(
    "PATH",
    RasterLiteDirectory + ";" + Environment.GetEnvironmentVariable("PATH"));

随后,DecodeTile 用 P/Invoke 解码 BLOB,读出浮点像元,并始终释放 native 资源:

bash 复制代码
var raster = NativeRasterLite.rl2_raster_decode(
    0x31, odd, odd.Length, even, even?.Length ?? 0, IntPtr.Zero);
if (raster == IntPtr.Zero)
    throw new InvalidOperationException("Cannot decode a RasterLite2 tile.");
 
try
{
    Ensure(NativeRasterLite.rl2_get_raster_size(raster, out var width, out var height));
    Ensure(NativeRasterLite.rl2_raster_data_to_float(raster, out var pointer, out _));
    try
    {
        var values = new float[checked((int)width * (int)height)];
        Marshal.Copy(pointer, values, 0, values.Length);
        // values -> Bitmap -> PNG -> Mapsui MRaster
    }
    finally
    {
        NativeCRuntime.free(pointer);
    }
}
finally
{
    NativeRasterLite.rl2_destroy_raster(raster);
}

对应声明:

bash 复制代码
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr rl2_raster_decode(
    int scale, byte[] odd, int oddSize, byte[]? even, int evenSize, IntPtr palette);
 
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int rl2_get_raster_size(
    IntPtr raster, out uint width, out uint height);
 
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int rl2_raster_data_to_float(
    IntPtr raster, out IntPtr buffer, out int bufferSize);
 
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern void rl2_destroy_raster(IntPtr raster);

0x31rl2_raster_data_to_float、NoData 值、像元尺寸和 free 的调用方式都与这份 DEM 和当前 native 构建绑定,不能直接当作所有 Coverage 的模板。特别是 native 内存必须由兼容的运行时释放;更换 RasterLite2 构建或 CRT 后,应以该库的 C API / 头文件为准重新核对分配与释放约定。

4.3 从 float 像元生成局部透明栅格

项目遍历每个像元中心,仅绘制落在选择多边形内、有限且不等于 NoData 的值:

bash 复制代码
var x = tileMinX + (column + 0.5) * PixelSize;
var y = tileMaxY - (row + 0.5) * PixelSize;
var value = values[row * bitmap.Width + column];
var center = new Point(x, y);
 
if (!_selection.Covers(center) || !float.IsFinite(value) || value == NoData)
    continue;
 
var gray = (byte)Math.Clamp(
    (int)Math.Round((value - DemMinimum) * 255 / (DemMaximum - DemMinimum)),
    0, 255);

由此生成的 MRaster 范围是:

bash 复制代码
var extent = new MRect(
    tileMinX,
    tileMaxY - height * PixelSize,
    tileMinX + width * PixelSize,
    tileMaxY);

其中 PixelSize = 10 与库中这一 DEM 的实际分辨率相符;应从 Coverage 元数据或输入数据读取,不应在通用程序中硬编码。

5. Coverage、Section、Tile 分层验证

验证层 本项目的证据 可以证明 不能证明
SQLite 文件 SqliteConnection.Open() 成功 数据库文件可按只读方式打开 RasterLite2 DLL、Coverage 或 Tile 可读
Coverage gdalinfo -json RASTERLITE2:dem.sqlite:dem 成功 GDAL 可识别并读取 dem Coverage 某个应用的原生 Tile 解码逻辑正确
完整影像 gdal_translate 成功并显示图层 Coverage 可导出为完整图像 局部 SQL 范围筛选正确
Tile 查询 dem_tilesdem_tile_data、空间索引联表返回行 目标范围内存在原始层级的 Tile BLOB BLOB 能成功解码
Tile 解码 rl2_raster_decode 非空、尺寸与 float 转换成功 本机构建可解码所取 Tile 颜色拉伸、NoData 和坐标范围正确
局部显示 Mapsui 叠加的 SQL tile 与完整 DEM 对齐 SQL 筛选、像元定位、渲染结果相互一致 写入/导入过程一定正确

dem_sections 虽然是 RasterLite2 的重要对象,但当前查看器不读取它。若需要验收"某份源影像是否已被导入",请在数据库工具中补查 dem_sections 的记录;那属于数据生产链路,不是本项目的显示链路。

6. 当前项目的边界与落地建议

  • 示例中的数据库、DLL、GDAL 和 Shapefile 目录均是绝对路径。发布前应移入配置文件或命令行参数,并在启动时逐项校验存在性。
  • SQL 参数已用于空间范围,避免了把坐标值直接拼进 SQL;Coverage 名称不能用参数绑定到表名,若要可选 Coverage,应从受信白名单选择并安全地构造标识符。
  • 通过 PATH 解析 native 依赖是进程级行为。桌面工具可以接受;生产部署应使用固定、版本一致的 native 目录,并记录 GDAL / RasterLite2 的版本。
  • 全图路径每次会导出一个临时 TIFF。若反复刷新,建议缓存 PNG 或在退出时清理自己创建的临时文件。
  • 这是 Windows x64 的实践。kernel32!SetDllDirectorymsvcrt!free 和 DLL 文件名都不是跨平台方案。

7. 最小验收清单

  1. dotnet build 成功。
  2. dem.sqlitelibrasterlite2.dllgdalinfo.exeGDAL_DATA 目录均存在。
  3. 启动后,"DEM (full RasterLite2)"图层能显示完整影像。
  4. 选择区域与 DEM 相交时,"DEM (SQL tile selection)"图层能显示,并可按需打开像元中心点图层。
  5. 两个 DEM 图层在同一范围内对齐;否则依次核查 Tile 查询范围、PixelSize、NoData、坐标系和 GDAL 的 Coverage 元数据。

一句话总结:本项目先让 GDAL 验证整个 RasterLite2 Coverage,再直接从 SQLite 读取 Tile BLOB 并交给 librasterlite2.dll 解码;二者叠加一致,才是当前 C# 查看链路可靠的完成标志。

参考资料

相关推荐
飞翔的火箭弹1 小时前
企业网络资产管理用什么系统工具
开发语言·网络·php
码云骑士1 小时前
116-Python调用GPT-4V分析图片-Base64编码-多图-JSON结构化输出
开发语言·python·json
88号技师1 小时前
2026年SCI-分数牛顿衍生优化算法Fractional Newton-derived optimizer-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
承渊政道1 小时前
【Python编程—从入门到实践】(Python操作列表深度教程:循环、切片、元组与代码风格)
开发语言·python·pycharm·列表·元组·切片
好名字都被猪取了-1 小时前
# C# 全栈开发资源包(.NET8/.NET10)
c#
布值倒区什么name1 小时前
Pycharm2026找不到编译器 点击运行运行不了
开发语言·python·pycharm
智购科技自动售货机工厂1 小时前
2026自动售货机电机驱动芯片选型:从L298N到DRV8870的工程实践~YH
大数据·开发语言·数据库·人工智能·单片机·嵌入式硬件·scikit-learn
2501_915918411 小时前
Rust 程序抓包解密,rustls 不认系统证书的几种办法
开发语言·后端·网络协议·ios·adb·https·rust
今天AI了吗2 小时前
AI 数据安全治理框架:模型能力与数据权限的边界在哪里
java·linux·开发语言·人工智能·python·深度学习·机器学习