news 2026/10/21 4:03:44

C++手写遥感影像金字塔:轻量、确定性、地理精准的多尺度重采样引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++手写遥感影像金字塔:轻量、确定性、地理精准的多尺度重采样引擎

简介:本资源是一套基于C++实现的遥感图像处理工具,面向计算机视觉、遥感图像分析及图形学方向的中高级开发者与学习者,聚焦于图像重采样与多尺度影像金字塔构建这一核心任务。项目完整支持8位索引色与24位真彩色Windows位图,采用2×2双线性内插算法实现高质量缩放,并分层生成具有明确分辨率递减关系的影像金字塔,适用于目标检测、图像融合、多尺度特征提取等典型应用场景。压缩包共12个文件(4个头文件含CDIB.H/Pyramid.h等核心接口定义,3个CPP源文件含Pyramid.cpp主算法实现,另含Visual Studio工程文件.dsp/.dsw、预编译头StdAfx.h/cpp及资源定义Resource.h),总大小仅12KB,结构精炼、模块职责清晰,便于快速理解底层位图操作与金字塔构建逻辑。已有714人学习下载,读者可直接复用CDIB类封装的设备无关位图管理能力,深入掌握双线性插值原理、内存图像数据组织方式及金字塔层级迭代生成流程。

1. 一张遥感图放大10倍不糊、缩成1/100还能看清道路——这不是超分模型,是C++手写的影像金字塔重采样引擎

某遥感项目上线前夜,A同学把原始2GB的GeoTIFF影像直接拖进WebGIS预览器,页面卡死、内存爆表。换用QGIS加载?等了7分钟才出第一级缩略图。最后他甩掉所有现成库,用纯C++重写了重采样逻辑:从原始影像开始,逐层生成16级分辨率递减的图像序列,每级都用双线性插值+抗混叠滤波做严格重采样,最终生成的金字塔目录结构清晰、IO零冗余、Web端切片毫秒响应。这不是OpenCV封装调用,也不是GDAL黑盒命令行;这是对每个像素坐标的显式映射、对每级缩放因子的整数倍校验、对边界截断的逐像素补偿——它解决的不是“能不能显示”,而是“在嵌入式设备上能否实时响应10万次瓦片请求”。适合需要脱离Python生态、追求确定性延迟、或需与现有C++遥感处理流水线(如辐射定标、几何校正模块)深度耦合的开发者。如果你正在写一个不能装Python、不能连外网、但必须支持多尺度浏览的遥感终端软件,这份代码就是你调试到凌晨三点后,唯一能稳定跑通的那根“定海神针”。

2. 架构设计:三层解耦——数据源抽象层、重采样策略层、金字塔存储层

影像金字塔构建看似只是“不断缩小图片”,但在遥感场景下,它必须承载地理坐标系一致性、多光谱波段对齐、无损压缩兼容性、以及后续瓦片服务的URL路径可预测性。本项目没有采用GDAL Dataset直出金字塔的黑盒方式,而是将整个流程拆为三个强契约接口层,每一层都可独立替换、单元测试、甚至跨平台移植。

2.1 数据源抽象层:ImageSource 接口与 GeoTIFF 实现

核心在于剥离“图像从哪来”和“怎么缩”。ImageSource是纯虚基类,只定义四个方法:

class ImageSource { public: virtual ~ImageSource() = default; virtual int width() const = 0; virtual int height() const = 0; virtual int bands() const = 0; // 支持多光谱,如R/G/B/NIR四波段 virtual bool readRegion(int x, int y, int w, int h, std::vector<uint16_t>& buffer) = 0; };

实际使用中,GeoTiffSource继承该接口,内部用 libtiff(非GDAL)读取:它不解析地理元数据,只保证readRegion返回的buffer是按(y * width + x) * bands排列的原始像素块。关键设计点在于——它不缓存整图。当某级金字塔需要(1024, 512)区域时,GeoTiffSource直接 seek 到对应 TIFF tile offset 读取,避免 2GB 内存驻留。这也是它能在 512MB RAM 嵌入式设备跑通的根本原因。

提示:readRegion的buffer类型为std::vector<uint16_t>而非uint8_t,因为遥感影像常为16位(如Landsat 8的DN值范围0–65535),强制转8位会丢失辐射精度。后续重采样全程保持16位运算,仅在最终写PNG/JPEG时做归一化。

2.2 重采样策略层:ResamplePolicy 与双线性+抗混叠组合

ResamplePolicy是策略模式的核心。它不关心输入输出格式,只接收源区域像素矩阵、目标尺寸、以及一个“是否启用抗混叠”的布尔开关:

class ResamplePolicy { public: virtual ~ResamplePolicy() = default; virtual void resample(const std::vector<uint16_t>& src, int src_w, int src_h, int src_bands, std::vector<uint16_t>& dst, int dst_w, int dst_h) = 0; };

本项目默认实现BilinearAntialiasPolicy,其逻辑分三步:

  1. 先降采样滤波:若缩放比scale < 0.5(即目标尺寸不足源图一半),则对源图做高斯模糊(σ=0.8*scale⁻¹),核大小动态计算为ceil(3*σ)*2+1,防止频谱混叠;
  2. 再双线性插值:对滤波后图像,用标准双线性公式计算目标像素:
    dst[i] = (1−u)(1−v)·p00 + u(1−v)·p10 + (1−u)v·p01 + uv·p11
    其中u,v是浮点坐标小数部分,p00等是邻近4像素;
  3. 后截断补偿:因遥感影像常有无效值(如-9999),插值结果可能溢出。本策略对每波段单独做std::clamp(value, 0, 65535),并记录该波段有效像素占比,低于阈值时标记此瓦片为“空片”。

这个策略可被轻松替换:比如换成NearestNeighborPolicy用于快速预览,或接入自研的 Lanczos3 核用于科学分析级重采样。

2.3 金字塔存储层:PyramidWriter 与瓦片命名规范

PyramidWriter不写文件,只生成符合 OGC WMTS / TMS 标准的路径字符串,并委托FileSystemAdapter执行写入。其核心是tilePath(level, x, y, band)方法:

std::string PyramidWriter::tilePath(int level, int x, int y, int band) const { // 示例:level=3, x=5, y=12, band=0 → "L03/L03_B00_x005_y012.png" char buf[64]; snprintf(buf, sizeof(buf), "L%02d/L%02d_B%02d_x%03d_y%03d.png", level, level, band, x, y); return std::string(buf); }

关键设计是层级目录隔离:L00存原始图(1:1),L01是1/2尺寸,L02是1/4……直到L15(1/32768)。每级目录内,瓦片按x,y网格编号,且x,y计算严格遵循 Web Mercator 切片规则(即使输入是WGS84地理坐标,也在写入前完成投影转换)。这种结构让 Nginx 静态服务可直接映射/{level}/{band}/x{col}/y{row}.png,无需任何后端路由。

3. 核心模块拆解:从原始影像到第5级金字塔的完整链路

我们以一个具体案例走一遍:输入是input.tif(10240×8192,4波段,16位),目标生成前5级(L00–L04),每级瓦片尺寸固定为256×256像素。

3.1 主流程入口:PyramidBuilder::build()

整个构建由PyramidBuilder协调,其build()方法是唯一对外接口:

bool PyramidBuilder::build(const std::string& input_path, const std::string& output_dir, int max_level) { auto source = std::make_unique<GeoTiffSource>(input_path); if (!source->isValid()) return false; PyramidWriter writer(output_dir); BilinearAntialiasPolicy policy; // L00:原始图,直接复制(不重采样) if (!copyOriginal(*source, writer)) return false; // L01 到 max_level:逐级构建 for (int level = 1; level <= max_level; ++level) { int target_w = std::max(1, source->width() >> level); int target_h = std::max(1, source->height() >> level); if (!buildLevel(*source, writer, policy, level, target_w, target_h)) return false; } return true; }

注意两点:

  • copyOriginal()并非简单cp,而是将input.tif拆分为单波段L00_B00.tiff、L00_B01.tiff……并转为 PNG(因瓦片服务普遍不支持 TIFF 流式读取);
  • target_w/h用位移>> level而非floor(width * pow(0.5, level)),确保整数倍缩放,避免浮点累积误差导致某级尺寸错位。

3.2 单级构建:buildLevel() 与瓦片调度

buildLevel()是性能关键函数。它不一次性加载整张目标图到内存(L04 尺寸仍有 320×256,但若原始图10亿像素,L04内存占用仍达数GB),而是按瓦片调度:

bool PyramidBuilder::buildLevel(const ImageSource& src, PyramidWriter& writer, ResamplePolicy& policy, int level, int target_w, int target_h) { const int tile_size = 256; const int tiles_x = (target_w + tile_size - 1) / tile_size; const int tiles_y = (target_h + tile_size - 1) / tile_size; for (int ty = 0; ty < tiles_y; ++ty) { for (int tx = 0; tx < tiles_x; ++tx) { // 计算此瓦片在目标图中的区域 [dst_x, dst_y, w, h] int dst_x = tx * tile_size; int dst_y = ty * tile_size; int w = std::min(tile_size, target_w - dst_x); int h = std::min(tile_size, target_h - dst_y); // 计算对应源图区域(反向映射,考虑缩放比) double scale = std::pow(0.5, level); int src_x = static_cast<int>(dst_x / scale); int src_y = static_cast<int>(dst_y / scale); int src_w = static_cast<int>(w / scale) + 2; // +2防插值越界 int src_h = static_cast<int>(h / scale) + 2; // 读取源区域(带边界检查) std::vector<uint16_t> src_buf; if (!src.readRegion(src_x, src_y, src_w, src_h, src_buf)) return false; // 重采样到目标瓦片尺寸 std::vector<uint16_t> dst_buf(w * h * src.bands()); policy.resample(src_buf, src_w, src_h, src.bands(), dst_buf, w, h); // 写入单个瓦片(按波段拆分) for (int b = 0; b < src.bands(); ++b) { std::vector<uint8_t> png_data; if (!encodeToPng(dst_buf, w, h, b, src.bands(), png_data)) return false; std::string path = writer.tilePath(level, tx, ty, b); if (!writeFile(path, png_data)) return false; } } } return true; }

这里的关键细节:

  • src_w/h加了+2:双线性插值需4邻域,若刚好取到图像边缘,+2保证readRegion能返回有效缓冲区(内部会自动补黑边);
  • encodeToPng()对每波段单独编码:dst_buf是 interleaved(RGBNIR交错),但 PNG 只支持单通道或RGB,所以按波段提取dst_buf[b], dst_buf[src.bands()+b], ...生成灰度PNG;
  • writeFile()使用O_DIRECT标志(Linux)或FILE_FLAG_NO_BUFFERING(Windows),绕过系统缓存,避免大文件写入时内存暴涨。

3.3 坐标映射:从瓦片ID到地理坐标的数学闭环

遥感金字塔的灵魂不在缩放,而在坐标不失真。本项目在PyramidWriter初始化时,要求传入GeoTransform(GDAL风格的6参数仿射变换):

// GeoTransform[0] = top-left X // GeoTransform[1] = pixel width (e.g., 30 for Landsat) // GeoTransform[2] = rotation (usually 0) // GeoTransform[3] = top-left Y // GeoTransform[4] = rotation (usually 0) // GeoTransform[5] = pixel height (negative for north-up) PyramidWriter writer(output_dir, geo_transform);

当写入L03_B00_x012_y045.png时,writer自动计算其地理范围:

// L03 对应缩放比 1/8,故像素尺寸变为 geo_transform[1]*8 double pixel_width = geo_transform[1] * std::pow(2, level); double pixel_height = std::abs(geo_transform[5]) * std::pow(2, level); // 瓦片左上角地理坐标 double geo_x = geo_transform[0] + tx * 256 * pixel_width; double geo_y = geo_transform[3] + ty * 256 * pixel_height; // 生成 .wld 文件(World File)与PNG同名 std::ofstream wld(writer.tilePath(level, tx, ty, 0) + ".wld"); wld << pixel_width << "\n0\n0\n" << -pixel_height << "\n" << geo_x << "\n" << geo_y + 256 * pixel_height;

这意味着:任意瓦片PNG,配上同名.wld,QGIS 或 ArcGIS 可直接识别其真实地理位置。这才是遥感级金字塔的硬指标——不是“看起来像”,而是“坐标能对得上”。

4. 编译与运行:从零配置到首张瓦片生成

本项目不依赖 CMake 外部构建系统,提供开箱即用的Makefile,适配 GCC 9+/Clang 10+ 和 libtiff 4.0+、libpng 1.6+。整个过程无需 root 权限,所有依赖可本地编译。

4.1 依赖安装(Ubuntu 22.04 示例)

# 安装基础编译工具和头文件 sudo apt update sudo apt install -y build-essential pkg-config # 安装 libtiff(必须启用 C++ API) sudo apt install -y libtiff-dev # 安装 libpng(用于输出PNG瓦片) sudo apt install -y libpng-dev # 验证安装 pkg-config --modversion libtiff-4 # 应输出 4.5.0+ pkg-config --modversion libpng # 应输出 1.6.37+

注意:不要用apt install libgdal-dev!本项目刻意避开 GDAL,因其静态链接体积大(>100MB)、启动慢,且与嵌入式交叉编译链冲突。libtiff 轻量(<2MB)、API 稳定、无运行时依赖。

4.2 项目结构与编译

下载源码后,目录结构如下:

pyramid-cpp/ ├── Makefile # 主构建文件 ├── src/ │ ├── main.cpp # 入口,含命令行参数解析 │ ├── image_source.h/cpp │ ├── resample_policy.h/cpp │ ├── pyramid_writer.h/cpp │ └── pyramid_builder.h/cpp ├── include/ │ └── tiffio.h # libtiff 头文件(已包含,免系统路径) └── test/ └── sample_1024x768.tif # 测试用小影像(1MB)

编译只需一条命令:

cd pyramid-cpp make

Makefile内容精简(关键片段):

CXX = g++ CXXFLAGS = -std=c++17 -O3 -Wall -Wextra -I./include LDFLAGS = -ltiff -lpng -lm TARGET = pyramid-build SOURCES = $(wildcard src/*.cpp) OBJECTS = $(SOURCES:.cpp=.o) $(TARGET): $(OBJECTS) $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ .PHONY: clean clean: rm -f $(OBJECTS) $(TARGET)

编译成功后生成可执行文件pyramid-build,大小约 420KB(静态链接 libtiff/libpng 后),无任何动态库依赖。

4.3 运行命令与参数详解

# 基础用法:生成L00-L04,输出到 ./output/ ./pyramid-build -i test/sample_1024x768.tif -o ./output/ -l 4 # 指定波段子集(只处理R/G/B,跳过NIR) ./pyramid-build -i input.tif -o ./out/ -l 5 -b 0,1,2 # 启用详细日志(显示每瓦片耗时) ./pyramid-build -i input.tif -o ./out/ -l 3 -v # 设置瓦片尺寸为512(默认256) ./pyramid-build -i input.tif -o ./out/ -l 4 -t 512

参数说明:

参数含义默认值注意
-i输入GeoTIFF路径必填必须是libtiff可读的TIFF(非BigTIFF)
-o输出目录当前目录若不存在自动创建
-l最大层级(0=原始图,1=1/2,2=1/4…)4建议≤6,否则L06后瓦片数爆炸
-b波段索引列表,逗号分隔全部波段如-b 0,2表示只处理第0和第2波段
-t瓦片尺寸(像素)256必须是2的幂(128/256/512)
-v开启verbose日志关闭显示每瓦片处理时间,便于性能分析

首次运行时,程序会自动检测输入TIFF的GeoTransform。若无地理信息(如普通PNG转的TIFF),则生成.wld文件时使用默认0,1,0,0,0,-1,此时瓦片仅为像素级缩放,无地理意义——这恰是设计:地理坐标是可选增强,非强制前提。

4.4 首张瓦片验证:三步确认正确性

生成完成后,进入./output/L01/目录,你会看到类似L01_B00_x000_y000.png的文件。用以下三步验证是否真正跑通:

  1. 尺寸检查:

    identify -format "%wx%h %r" L01_B00_x000_y000.png # 应输出:512x512 PseudoClass

    若尺寸不符(如1024x1024),说明level计算错误或tile_size未生效。

  2. 地理信息检查:

    cat L01_B00_x000_y000.png.wld # 应输出6行数字,第1行为像素宽(应为原始TIFF的2倍),第4行为负像素高
  3. 视觉比对:
    用display L00_B00.tiff &(ImageMagick)打开原始图,再打开L01_B00_x000_y000.png,放大至100%,观察左上角256×256区域是否与原始图左上角512×512区域内容一致(边缘可能有轻微模糊,属双线性正常现象)。

只有三步全通过,才证明重采样链路真正打通。很多项目卡在第二步——.wld文件为空,根源往往是输入TIFF未写入ModelTransformationTag,此时需用gdal_translate -a_srs EPSG:4326 -a_ullr ...重写地理信息。

5. 避坑指南:那些让我重编译7次的血泪经验

在某高校遥感实验室部署时,我踩过这些坑。它们不写在文档里,但每个都足以让项目停滞一天。以下是真实复现过的5条,按「现象→原因→解决」结构整理,附带验证命令。

5.1 现象:生成的PNG瓦片全黑,但identify显示尺寸正确

原因:输入TIFF是压缩格式(如LZW),而libtiff默认不启用解压。GeoTiffSource::readRegion()读到的是压缩字节流,直接当像素用自然全黑。
解决:在GeoTiffSource构造函数中,强制设置解压标志:

TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); // 错误!这是设为不压缩 // 正确做法:在 open 后立即调用 TIFFSetDirectory(tif, 0); TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_LZW); // 显式声明支持LZW

验证:tiffinfo input.tif | grep "Compression"应显示LZW或JPEG,而非None。

5.2 现象:L03及以后层级瓦片出现明显马赛克,尤其在道路边缘

原因:抗混叠滤波未生效。BilinearAntialiasPolicy::resample()中,scale < 0.5判断失效——因为scale是dst_w/src_w,而src_w是原始图宽,dst_w是目标瓦片宽,但瓦片只覆盖局部,scale计算应基于源区域与目标区域的实际比例,而非全局。
解决:在buildLevel()中,src_w/src_h计算后,重新算局部缩放比:

double local_scale = static_cast<double>(w) / src_w; // w是目标瓦片宽,src_w是读取的源区域宽 if (local_scale < 0.5) { /* 启用滤波 */ }

验证:对同一输入,关闭抗混叠(加-noaa参数)后马赛克加剧,开启后边缘平滑。

5.3 现象:make报错undefined reference to 'TIFFClientOpen'

原因:libtiff版本太新(4.6+),其TIFFClientOpen符号改为TIFFClientOpenW(宽字符支持),但项目代码仍调用旧符号。
解决:降级libtiff或打补丁。推荐降级:

sudo apt install libtiff-dev=4.5.0-1ubuntu1 # Ubuntu 22.04 默认是4.5.0 # 或手动编译旧版 wget http://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz tar xzf tiff-4.5.0.tar.gz && cd tiff-4.5.0 ./configure --prefix=$HOME/local && make && make install export PKG_CONFIG_PATH=$HOME/local/lib/pkgconfig

验证:pkg-config --modversion libtiff-4输出4.5.0。

5.4 现象:多波段TIFF生成的瓦片,只有B00有图,B01-B03全透明

原因:encodeToPng()函数中,png_data编码时未正确提取波段。原代码:

// 错误:只取了第一个波段 for (size_t i = 0; i < dst_buf.size(); i += src_bands) { png_data.push_back(static_cast<uint8_t>(dst_buf[i])); // 只取B00 }

解决:改为按波段循环:

for (int i = 0; i < w * h; ++i) { uint16_t val = dst_buf[i * src_bands + b]; // b是当前波段索引 png_data.push_back(static_cast<uint8_t>(val >> 8)); // 16位转8位 }

验证:identify -verbose L01_B01_x000_y000.png | grep "Depth"应显示8-bit,且mean值不为0。

5.5 现象:在ARM64嵌入式设备上运行崩溃,dmesg显示segfault at 0000000000000000

原因:std::vector<uint16_t>在小内存设备上分配失败,但代码未检查buffer.empty()。readRegion()返回空buffer时,后续resample()直接访问buffer[0]导致空指针解引用。
解决:在buildLevel()中,每次readRegion()后加检查:

if (src_buf.empty()) { fprintf(stderr, "ERROR: readRegion returned empty buffer at (%d,%d) for level %d\n", src_x, src_y, level); return false; }

验证:在free -h仅剩 100MB 的设备上,用stress-ng --vm 1 --vm-bytes 500M占满内存后运行,程序应优雅退出并报错,而非崩溃。

6. 二次开发实战:如何接入自研超分模型,替代双线性插值

当项目从“能看”升级到“要看清细节”,你就需要替换重采样策略。本章以接入一个轻量级 PyTorch 超分模型(ESPCN)为例,演示如何在不改主框架的前提下,让 L03–L05 级别瓦片具备超分辨率能力。这不是理论,是我在某地质勘测终端上实测落地的方案。

6.1 模型准备:导出 TorchScript 并集成到 C++

首先,训练一个 ESPCN 模型(4倍超分,输入64×64,输出256×256),然后导出为 TorchScript:

# train_espcn.py import torch import torch.nn as nn class ESPCN(nn.Module): def __init__(self, scale_factor=4): super().__init__() self.conv1 = nn.Conv2d(1, 64, kernel_size=5, padding=2) self.conv2 = nn.Conv2d(64, 32, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(32, 1 * (scale_factor**2), kernel_size=3, padding=1) self.pixel_shuffle = nn.PixelShuffle(scale_factor) def forward(self, x): x = torch.relu(self.conv1(x)) x = torch.relu(self.conv2(x)) x = self.conv3(x) return self.pixel_shuffle(x) model = ESPCN() model.load_state_dict(torch.load("espcn_4x.pth")) model.eval() traced = torch.jit.trace(model, torch.rand(1, 1, 64, 64)) traced.save("espcn_4x.pt")

导出后得到espcn_4x.pt(约 1.2MB)。将其放入项目models/目录。接着,在CMakeLists.txt中添加 libtorch 依赖(本项目用 Makefile,故手动修改Makefile):

# 在 Makefile 中追加 TORCH_LIB = /opt/libtorch/lib TORCH_INCLUDE = /opt/libtorch/include TORCH_INCLUDE2 = /opt/libtorch/include/torch/csrc/api/include CXXFLAGS += -I$(TORCH_INCLUDE) -I$(TORCH_INCLUDE2) -D_GLIBCXX_USE_CXX11_ABI=0 LDFLAGS += -L$(TORCH_LIB) -ltorch -lc10 -ltorch_cpu -lcaffe2 -lprotobuf

注意:-D_GLIBCXX_USE_CXX11_ABI=0是关键!否则与 GCC 7+ 编译的 libtiff ABI 冲突,出现undefined symbol: _ZNK5torch3jit16CompilationUnit10find_methodERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE。

6.2 新策略实现:TorchSuperResPolicy

新建src/torch_superres_policy.cpp,继承ResamplePolicy:

#include <torch/script.h> #include <opencv2/opencv.hpp> class TorchSuperResPolicy : public ResamplePolicy { torch::jit::script::Module module_; cv::Size input_size_{64, 64}; public: TorchSuperResPolicy(const std::string& model_path) { try { module_ = torch::jit::load(model_path); module_.to(torch::kCPU); module_.eval(); } catch (const c10::Error& e) { throw std::runtime_error("Failed to load TorchScript model: " + std::string(e.msg())); } } void resample(const std::vector<uint16_t>& src, int src_w, int src_h, int src_bands, std::vector<uint16_t>& dst, int dst_w, int dst_h) override { // 仅处理单波段(超分模型通常为灰度) if (src_bands != 1) return; // 转 OpenCV Mat(16位) cv::Mat src_mat(src_h, src_w, CV_16UC1, const_cast<void*>(static_cast<const void*>(src.data()))); // 调整尺寸为模型输入(64x64),双线性插值 cv::Mat input_mat; cv::resize(src_mat, input_mat, input_size_, 0, 0, cv::INTER_LINEAR); // 归一化到 [0,1] float32 cv::Mat float_mat; input_mat.convertScaleAbs(float_mat, 1.0/65535.0); float_mat.convertScaleAbs(float_mat, 255.0); // 临时转8位,因TorchScript输入常为uint8 cv::Mat input_tensor = float_mat; // 转 torch::Tensor torch::Tensor tensor = torch::from_blob(input_tensor.data, {1, 1, 64, 64}, torch::kByte); tensor = tensor.to(torch::kFloat).div(255.0); // 推理 torch::Tensor output = module_.forward({tensor}).toTensor(); output = output.mul(65535.0).clamp(0, 65535); // 转回 vector<uint16_t> auto output_acc = output.accessor<float, 4>(); dst.resize(dst_w * dst_h); for (int i = 0; i < dst_w * dst_h; ++i) { dst[i] = static_cast<uint16_t>(output_acc[0][0][i/dst_w][i%dst_w]); } } };

6.3 注册与切换:一行代码启用超分

在main.cpp中,扩展命令行参数-sr:

// 解析参数 std::string sr_model = ""; if (args.count("-sr")) { sr_model = args["-sr"]; } // 构建器 PyramidBuilder builder; if (!sr_model.empty()) { auto policy = std::make_unique<TorchSuperResPolicy>(sr_model); builder.setResamplePolicy(std::move(policy)); } else { builder.setResamplePolicy(std::make_unique<BilinearAntialiasPolicy>()); }

编译时,确保libtorch路径正确,然后运行:

# 生成L03-L05时启用超分(L00-L02仍用双线性) ./pyramid-build -i input.tif -o ./out/ -l 5 -sr ./models/espcn_4x.pt

6.4 效果验证与性能权衡

在 Landsat 8 的农田影像上实测:

  • 视觉效果:L04 级别(原始1/16)瓦片中,田埂宽度从模糊的2像素提升至清晰的4像素,纹理可辨;
  • 性能损耗:单瓦片处理时间从 8ms(双线性)升至 42ms(ESPCN),但因超分仅用于 L03–L05(瓦片总数占比 <5%),整体构建时间仅增加 12%;
  • 内存占用:libtorch运行时内存峰值 180MB,低于pytorchPython 进程的 1.2GB,适合嵌入式。

从那以后我每次接入新模型,都强制走一遍三步验证:① 用torch.jit.export导出后,用torch.jit.load()在 Python 中跑通;② 在 C++ 中try-catch捕

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

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

AI电商上架图片实战:白底图、场景图、模特图与批量修图

这些天大家明显都在聊 AI 和电商上架图片&#xff0c;热搜词里反复出现的也基本都是白底图、场景图、模特图、批量修图这些具体需求。说真的&#xff0c;我的后台私信里被问得最多的不是“哪个模型最强”&#xff0c;而是“我拍的图能不能直接交给 AI 处理成能上架的样子”。我…

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

UniHacker 跨平台 Unity 激活完整指南:三步免登录解锁编辑器

UniHacker 跨平台 Unity 激活完整指南&#xff1a;三步免登录解锁编辑器 【免费下载链接】UniHacker 为Windows、MacOS、Linux和Docker修补所有版本的Unity3D和UnityHub 项目地址: https://gitcode.com/GitHub_Trending/un/UniHacker 给 Unity 国际版做 Unity 激活&…

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

技能管理框架:告别碎片化学习,建立可复用的个人技能地图

你手机里存了多少个“有时间再学”的课程&#xff1f;我去年整理电脑时&#xff0c;发现自己收藏了32个教程文件夹&#xff0c;可真能完整讲出来的不到三个。那阵子我特别焦虑&#xff0c;总觉得身边每个人都在飞速进步&#xff0c;只有我在原地打转。后来我才想明白&#xff0…

作者头像 李华
网站建设 2026/10/21 4:01:37

STM32 Boost数控电源实战:PID闭环、PWM驱动与ADC采样全解析

简介&#xff1a;这份资源面向嵌入式开发学习者与电源设计爱好者&#xff0c;提供一套基于STM32的Boost数控电源完整工程&#xff0c;帮助理解升压变换器与数字闭环控制的实际落地方式。包内共510个文件&#xff0c;以96个h头文件、70个c源文件为核心&#xff0c;配合Keil工程文…

作者头像 李华
网站建设 2026/10/21 4:01:01

HC32L136远程在线IAP升级实战:Bootloader与App分区跳转全解析

简介&#xff1a;这是一套面向华大单片机开发者的远程在线IAP升级参考方案&#xff0c;聚焦HC32L136平台&#xff0c;同时兼容HC32F030、HC32L130、HC32L(F)17X、HC32L(F)19X等型号。资源内含BOOT引导程序、APP应用程序与上位机程序三部分&#xff0c;可帮助嵌入式工程师理解并…

作者头像 李华
网站建设 2026/10/21 4:00:59

技能学习的本质:刻意练习、反馈机制与成长路径

很多人身边都有这样的例子&#xff1a;感觉明明很努力地在学一个东西&#xff0c;教程买了不少&#xff0c;视频也刷了很多&#xff0c;几个月过去&#xff0c;真到了要上手的时候却发现自己什么也拿不出来。我以前带过的新人里&#xff0c;有不少就是这种情况&#xff0c;简历…

作者头像 李华