简介:一套开箱即用的遥感影像Web处理工具,专注TIF格式遥感图的上传、自动镶嵌拼接与下载。后端基于若依(RuoYi)框架构建,内置用户权限管理、部门/岗位/角色配置、菜单与字典维护、系统参数设置等基础运维能力;支持操作日志记录、登录行为审计、在线用户实时监控、定时任务调度等功能。配套提供SQL初始化脚本(image_mosaic.sql)、Windows启动脚本(ry.bat、run.bat)、Linux部署脚本(ry.sh),以及环境说明和详细README文档。源码按模块清晰划分:image-system负责核心业务逻辑,image-quartz支撑定时任务,image-generator集成可视化代码生成器,可一键导出Java后端、HTML前端、XML配置及SQL建表语句。所有模块均适配Windows与Linux双环境,结构规范、注释完整,便于二次开发与功能扩展。
1. 项目概述:为什么一个遥感影像拼接系统需要“若依”打底?
你有没有遇到过这样的场景:手头有十几景 Sentinel-2 或 Landsat 的 TIF 影像,覆盖同一区域但存在重叠与缝隙,想快速合成一张无缝的区域图——不是用 ArcGIS 点半天菜单、等一小时渲染,也不是写一段 Python 脚本跑完还得手动调坐标系、填 NoData 值、再导出为 Web 可用格式。你需要的是:上传 → 点击“开始拼接” → 等 2~5 分钟 → 下载结果 TIF 或 PNG。整个过程不需要打开专业桌面软件,不依赖本地算力,更不需 Python 环境配置。这就是本系统要解决的真实痛点。
而之所以选择若依(RuoYi)框架作为底座,并非跟风,而是经过三轮实测对比后的理性决策。我曾用 Spring Boot 原生搭过一版纯影像拼接服务,接口干净、性能不错,但上线三天就被运维同事叫停:“用户在哪改投影参数?谁来禁用那个误传了 20GB 影像的账号?日志查不到是谁在凌晨三点触发了拼接任务?”——问题不在影像算法,而在系统可运维性缺失。若依不是“又一个后台模板”,它是一套被国内数百个政企项目反复锤炼过的生产级基础设施骨架:权限模型完整(RBAC+数据权限)、菜单动态加载、字典热更新、定时任务可视化启停、操作日志自动埋点、在线用户强制下线……这些能力不是锦上添花,而是遥感Web系统走向可用、可控、可审计的必经门槛。
关键词里排第一位的“遥感拼接”,本质是地理空间数据处理,核心诉求是几何精度保真 + 光谱一致性 + 大文件鲁棒处理;而“TIF镶嵌”特指 GeoTIFF 格式下的多景影像无缝融合,涉及坐标系统一(WGS84 / UTM / CGCS2000)、重采样策略(双线性/三次卷积)、接边羽化(feathering)、NoData 区域智能识别与填充。这些能力若依本身不提供,但它的模块化结构(image-system 专注业务、image-mosaic 封装算法、image-quartz 托管耗时任务)恰好为这类专业功能提供了清晰的插入点。至于“Web影像处理”,它拒绝把浏览器当摆设——所有交互必须响应明确(上传进度条、拼接状态轮询、失败原因高亮)、结果必须即得(支持直接预览 PNG 缩略图、下载原始 TIF、获取 WMS 地址),这才是工程落地的语言。
最后,“代码生成器”常被误解为“偷懒工具”,但在本系统中,它是降低二次开发门槛的关键杠杆。比如你要新增一个“影像质量评估”模块:上传后自动计算云量、信噪比、辐射定标系数有效性——只需在数据库建好 quality_report 表,用若依生成器一键产出 Controller/Service/DAO/HTML/SQL,再在 image-mosaic 模块里注入你的 Python 评估脚本调用逻辑。整个过程 15 分钟内完成,无需纠结 MyBatis XML 写法或 Vue 页面路由配置。这背后是若依对 CRUD 通用模式的高度抽象,而非对业务逻辑的越俎代庖。
所以,这不是一个“若依 + 遥感”的简单拼凑,而是一个以业务闭环为驱动、以运维友好为底线、以扩展敏捷为设计目标的工程实践。接下来,我会带你一层层拆开它的筋骨,告诉你每一行脚本为什么这么写、每个模块为何这样切分、每处配置背后藏着什么坑——就像当年我第一次部署它时,对着 ry.sh 里那行 JAVA_OPTS="-Xms2g -Xmx4g" 抓耳挠腮,直到搞懂 JVM 堆内存与 GDAL 内存映射的冲突关系。
2. 整体架构与模块职责解剖:为什么模块名都带“image-”前缀?
若依官方版本的模块命名习惯是 ruoyi-system、ruoyi-quartz,而本系统全部改为 image-system、image-quartz……这个看似微小的改动,实则是架构设计的第一个关键决策。它传递的信号很明确:这不是若依的二次开发,而是一个以遥感影像为核心领域的新系统,若依只是其基础设施提供者。所有模块名前缀统一为 image-,既是领域标识,也是心理锚点——提醒开发者:“你写的不是通用后台代码,而是遥感数据流中的一环”。
我们来看资源包目录树里的核心模块如何各司其职:
-
image-system:这是系统的“大脑皮层”。它不直接处理像素,但负责所有业务流程编排:接收前端上传请求、校验 TIF 文件头信息(GDAL Info 解析坐标系/波段数/数据类型)、生成唯一任务ID、将任务入库存储、触发image-quartz中的拼接Job、推送WebSocket状态更新、组装下载链接。它的 Controller 层非常薄,核心逻辑全在 ServiceImpl 中,且大量使用@Transactional保证“上传→入库→调度”原子性。特别注意它的异常处理机制:对 GDAL 解析失败、坐标系不匹配、文件损坏等遥感特有错误,会抛出自定义ImageException并携带ERROR_CODE(如TIF_CRS_MISMATCH=1002),前端据此精准提示“影像坐标系不一致,请统一为 EPSG:4326”,而非笼统的“系统错误”。 -
image-mosaic:真正的“影像处理器”,也是本系统的技术心脏。它不依赖 Spring 上下文,是一个独立的 Java 模块,通过 JNI 或进程调用方式与 Python 脚本协同工作。目录下img_mosaic.py是核心算法入口,采用 Rasterio + GDAL + NumPy 组合技:先用rasterio.open()安全读取多景影像元数据,构建统一输出范围(rasterio.coords.BoundingBox),再用rasterio.merge.merge()执行底层 C++ 镶嵌(支持method='first'/'last'/'min'/'max'/'mean'),最后用rasterio.warp.reproject()统一输出坐标系。这里有个关键细节:merge()默认不处理 NoData,所以脚本中必须显式设置src_nodata=0(或根据实际影像设定)并启用dst_nodata=0,否则拼接边缘会出现刺眼的黑色锯齿。该模块还内置了“接边优化”开关:开启时自动计算重叠区均值差异,用加权平均法平滑过渡,实测对 Landsat 8 OLI 影像效果显著。 -
image-quartz:不是简单包装 Quartz,而是实现了遥感任务专属的调度语义。标准 Quartz 的 JobDetail 只能绑定一个类,但遥感拼接任务需动态传入:输入文件路径列表、输出 CRS、重采样方法、是否启用羽化、超时阈值(单位:分钟)。因此,本系统重写了JobFactory,让每个 Job 实例都能从数据库sys_job表中读取job_paramsJSON 字段,并反序列化为MosaicJobParam对象。更重要的是,它集成了任务熔断机制:若某次拼接因内存溢出失败,下次调度前会自动检查服务器剩余内存(Runtime.getRuntime().freeMemory()),低于阈值则跳过本次执行并记录告警日志——避免连续失败拖垮整台服务器。 -
image-generator:若依原生代码生成器已足够强大,但本系统做了两项增强:第一,在“表配置”页面新增“遥感字段类型”下拉选项(如geo_bbox、crs_code、band_count),生成的 Entity 类会自动添加@GeoBoundingBox或@CrsCode注解,为后续 GIS 查询预留扩展点;第二,生成的 HTML 页面默认集成Leaflet地图预览组件,当用户查看某条影像记录时,自动解析其geo_bbox字段并在地图上绘制边界框——这使得“影像管理”真正具备空间感知能力,而非仅是文件列表。
提示:模块间通信严格遵循“松耦合”原则。
image-system从不直接 newimage-mosaic的类,而是通过定义MosaicService接口,由 Spring 的@Qualifier("gdalMosaicService")注入具体实现。这种设计让你未来可无缝替换为JAI-EXT或GeoTools实现,而无需修改业务流程代码。
3. 核心功能实现详解:从上传到拼接完成的完整链路
现在我们聚焦最核心的业务闭环:用户点击“上传影像”按钮,到最终收到拼接完成通知,中间发生了什么?这不是简单的 HTTP 请求转发,而是一条横跨前端、网关、业务层、算法层、存储层的精密流水线。下面我以一次典型 Landsat 8 影像拼接为例(3 景,每景约 800MB),逐环节还原真实执行过程。
3.1 前端上传与预校验:不只是“选文件”
用户在 image-admin 的 Vue 页面点击上传,触发的是一个经过深度定制的 el-upload 组件。它做了三件事:
-
客户端尺寸预检:通过
File.size限制单文件 ≤ 2GB(防止用户误传原始 Level-1 数据包),并在上传前计算 MD5(使用spark-md5库),发送至/api/image/checkMd5接口。该接口查询image_file_info表,若发现相同 MD5 已存在,则直接返回“文件已存在,是否复用?”,避免重复上传与存储。 -
TIF 头部探针:利用
tiff.js库在浏览器端解析 TIF 文件头,提取关键元数据:ImageWidth/ImageLength(分辨率)、BitsPerSample(位深)、SamplesPerPixel(波段数)、GeoKeyDirectoryTag(是否存在地理信息)。若检测到无地理信息(即普通 TIFF),前端立即拦截并提示“请上传含地理坐标的 GeoTIFF 文件”。 -
分片上传与断点续传:对 >100MB 的文件启用分片(每片 5MB),使用
Blob.slice()切割,并携带file_id、chunk_index、total_chunks参数。后端image-system的FileUploadController接收后,先校验分片 MD5,再写入临时目录upload/temp/{file_id}/。所有分片上传完成后,触发mergeChunks()方法,按序合并为完整 TIF,并调用 GDAL 进行最终校验。
注意:
mergeChunks()不是简单cat拼接!它会重新计算 TIFF 的 IFD(Image File Directory)偏移量,并修复StripOffsets标签,否则合并后的文件 GDAL 无法正确读取。这部分逻辑封装在TiffMergerUtil工具类中,已通过 50+ 种不同压缩方式(LZW/DEFLATE/None)的 TIF 测试。
3.2 后端任务创建与持久化:确保“不丢任务”
前端校验通过后,向 /api/mosaic/createTask 发送 POST 请求,携带参数:
{
"inputFiles": ["file_id_001", "file_id_002", "file_id_003"],
"outputCrs": "EPSG:4326",
"resampleMethod": "bilinear",
"enableFeathering": true,
"timeoutMinutes": 30
}
MosaicTaskServiceImpl.createTask() 方法执行以下原子操作:
-
事务内创建任务记录:插入
mosaic_task表,状态为WAITING,同时生成全局唯一task_code(格式:MOS-{YYYYMMDD}-{6位随机}),并初始化progress=0、start_time=null。 -
关联输入文件:批量插入
mosaic_task_file关联表,记录每景影像的file_id及其在拼接中的顺序权重(用于后续接边优化计算)。 -
发布任务事件:调用
applicationEventPublisher.publishEvent(new MosaicTaskCreatedEvent(taskId))。此事件被MosaicTaskListener监听,触发真正的调度逻辑。
关键细节:
mosaic_task表的status字段采用ENUM('WAITING','RUNNING','SUCCESS','FAILED','TIMEOUT')而非字符串,数据库层面约束状态流转合法性。且所有状态变更必须通过updateStatus(Long taskId, String oldStatus, String newStatus)方法,该方法内部使用UPDATE ... WHERE status = ?确保并发安全——避免两个线程同时将WAITING改为RUNNING导致重复执行。
3.3 算法层执行:Python 脚本如何与 Java 协同?
当 MosaicTaskListener 捕获到新任务,它不直接执行 Python,而是调用 MosaicExecutor.executeAsync(taskId)。该方法的核心逻辑如下:
public void executeAsync(Long taskId) {
// 1. 从DB加载任务详情,构造Python命令参数
MosaicTask task = mosaicTaskMapper.selectById(taskId);
String cmd = String.format(
"python %s/img_mosaic.py --task-id %d --input-files %s --output-crs %s --resample %s",
pythonScriptRoot,
taskId,
String.join(",", task.getInputFiles()),
task.getOutputCrs(),
task.getResampleMethod()
);
// 2. 使用ProcessBuilder启动子进程,设置超时与环境变量
ProcessBuilder pb = new ProcessBuilder("bash", "-c", cmd);
pb.environment().put("GDAL_DATA", "/opt/gdal/share/gdal"); // 关键!指定GDAL数据路径
pb.directory(new File("/tmp/mosaic_work")); // 工作目录隔离
// 3. 异步执行并监听输出流
Process process = pb.start();
Future<?> future = executorService.submit(() -> {
try {
int exitCode = process.waitFor(30, TimeUnit.MINUTES); // 30分钟硬超时
if (exitCode != 0) throw new RuntimeException("Python script failed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
process.destroyForcibly();
updateTaskStatus(taskId, "TIMEOUT");
}
});
}
img_mosaic.py 脚本的执行流程才是重头戏:
-
安全加载影像:使用
rasterio.Env(gdal_version='auto', CPL_DEBUG=False)初始化 GDAL 环境,关闭调试日志避免污染 Java 日志。遍历输入文件,对每个调用rasterio.open(path, 'r'),捕获rasterio.errors.RasterioIOError异常并记录详细错误(如Cannot open xxx.tif: No such file or directory)。 -
智能范围计算:不简单取所有影像 bbox 的 union,而是调用
rasterio.coords.BoundingBox(*rasterio.transform.array_bounds(height, width, transform))精确计算每景的实际地理范围,再求交集作为输出范围。这对处理斜向扫描的 SAR 影像至关重要。 -
内存敏感拼接:
rasterio.merge.merge()默认将所有影像加载进内存,3 景 800MB 影像会瞬间吃光 16GB 内存。因此脚本中显式设置bounds和resampling参数,并启用num_workers=2(避免过多线程争抢 GDAL 锁),最关键的是设置dtype=np.float32统一数据类型,减少内存占用。 -
结果写入与元数据注入:输出 TIF 时,不仅写入像素数据,还通过
rasterio.profiles.DefaultGTiffProfile()设置compress='lzw'、tiled=True、blockxsize=256、blockysize=256,生成真正适合 Web 服务的金字塔式 GeoTIFF。同时,将原始影像的tags()(如IMAGE_DESCRIPTION,DATE_ACQUIRED)合并写入输出文件的TAGS元数据,确保溯源性。
3.4 状态同步与结果交付:让用户“看得见、摸得着”
拼接成功后,img_mosaic.py 会在 /tmp/mosaic_work/{task_id}/output.tif 生成结果,并向 http://localhost:8080/api/mosaic/updateStatus 发送回调请求(携带 task_id 和 status=SUCCESS)。MosaicTaskController.updateStatus() 接收后:
- 更新
mosaic_task表状态为SUCCESS,记录end_time和output_path(转换为 Web 可访问路径,如/download/mosaic/MOS-20240520-ABC123.tif); - 触发
WebSocket推送:向用户连接的@MessageMapping("/topic/task/{userId}")发送 JSON 消息,包含task_code、progress=100、downloadUrl; - 自动生成缩略图:调用
ThumbnailGenerator.createPngThumbnail(outputPath, thumbnailPath, 800, 600),使用ImageIO读取 GeoTIFF 的 RGB 波段(若为多光谱则自动合成假彩色),生成 800x600 PNG 存于static/thumbnail/目录,供前端<img>标签直接引用。
实操心得:WebSocket 在高并发下易出现连接中断。本系统采用“双重确认”机制:前端收到消息后,主动发起
/api/mosaic/getTaskStatus?taskId={id}查询,若返回SUCCESS才显示下载按钮;若查询失败,则启动轮询(间隔 3s,最多 10 次)。这比单纯依赖 WebSocket 更健壮。
4. 部署与运维实战:从 Windows 开发机到 Linux 生产服务器
部署不是复制粘贴脚本,而是理解每个参数背后的物理意义。我见过太多团队卡在 ry.bat 第一行 set JAVA_HOME=C:\Program Files\Java\jdk-11.0.12 —— 因为他们的 JDK 装在 D:\jdk。下面我以真实生产环境(CentOS 7.9 + OpenJDK 11 + GDAL 3.4.3)为例,逐行解读 ry.sh 并给出避坑指南。
4.1 Linux 部署脚本 ry.sh 深度解析
#!/bin/bash
# ry.sh - 若依遥感拼接系统 Linux 启动脚本
# ========== 1. 环境变量配置 ==========
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.19.10-1.el7_9.x86_64
export PATH=$JAVA_HOME/bin:$PATH
export GDAL_DATA=/usr/share/gdal/3.4 # 关键!GDAL 必须能找到投影定义文件
export PROJ_LIB=/usr/share/proj # 同样关键!PROJ 库路径
# ========== 2. JVM 参数调优 ==========
# -Xms2g -Xmx4g:初始堆2G,最大堆4G。为什么不是8G?因为GDAL内存映射会额外占用Native Memory
# -XX:MetaspaceSize=256m:元空间设为256MB,避免频繁GC
# -XX:+UseG1GC:G1垃圾收集器更适合大堆与低延迟场景
# -Dfile.encoding=UTF-8:强制文件编码,防止中文路径乱码
JAVA_OPTS="-Xms2g -Xmx4g -XX:MetaspaceSize=256m -XX:+UseG1GC -Dfile.encoding=UTF-8"
# ========== 3. 应用参数 ==========
APP_NAME="image-system"
APP_JAR="/opt/remote-sensing/image-system/target/image-system.jar"
LOG_PATH="/var/log/remote-sensing"
# ========== 4. 启动逻辑 ==========
cd $(dirname $0)/..
mkdir -p $LOG_PATH
# 检查端口占用(8080)
if lsof -i :8080 > /dev/null; then
echo "Port 8080 is occupied. Please stop the process first."
exit 1
fi
# 启动应用,日志分离 stdout/stderr
nohup java $JAVA_OPTS -jar $APP_JAR > $LOG_PATH/app.log 2>&1 &
# 记录PID
echo $! > $LOG_PATH/app.pid
echo "Application $APP_NAME started with PID $!"
关键参数说明与踩坑记录:
-
GDAL_DATA和PROJ_LIB:这是最常被忽略的两行。GDAL 3.x 默认从GDAL_DATA加载epsg.wkt、gcs.csv等坐标系定义文件;PROJ 从PROJ_LIB加载proj.db。若未设置,rasterio.open()会报错CRSError: Unable to lookup EPSG:4326。CentOS 上可通过yum install gdal-devel proj-devel安装,并用find /usr -name "epsg.wkt"确认路径。 -
JAVA_OPTS中的-Xmx4g:不要盲目设为-Xmx8g!GDAL 使用内存映射(mmap)加载大 TIF,这部分内存不计入 JVM Heap,但会占用系统物理内存。一台 16GB 内存的服务器,建议 JVM Heap ≤ 4g,留给 GDAL 至少 8GB。实测若设为-Xmx8g,3 景拼接时系统会触发 OOM Killer 杀死 Java 进程。 -
nohup启动:生产环境必须用nohup或systemd管理进程,禁止前台运行。ry.sh末尾的echo $! > app.pid是为了后续stop脚本能精准 kill 进程。
4.2 Windows 开发环境适配要点
ry.bat 与 run.bat 的区别在于:ry.bat 是完整启动(含 image-quartz、image-generator),run.bat 仅启动 image-system 用于快速调试。Windows 用户需特别注意:
-
GDAL for Windows 安装:必须下载 OSGeo4W 安装 GDAL 3.4+,并勾选
python-rasterio、python-numpy。安装后,将C:\OSGeo4W64\bin加入系统 PATH,并设置GDAL_DATA=C:\OSGeo4W64\share\gdal。 -
路径分隔符陷阱:Java 中
File.separator在 Windows 是\,但 GDAL 的rasterio.open()期望/。因此img_mosaic.py中所有路径拼接必须用os.path.join()或pathlib.Path,避免硬编码'\'。 -
防病毒软件干扰:Windows Defender 或 360 会将
img_mosaic.py的 GDAL 调用标记为“可疑行为”并阻止。需将项目目录加入白名单,或临时禁用实时防护(仅开发时)。
4.3 数据库初始化与 SQL 脚本精讲
image_mosaic.sql 不是简单建表,而是针对遥感场景做了专项优化:
-- 1. 影像文件主表:增加空间索引与JSON字段
CREATE TABLE image_file_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
file_name VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
md5 CHAR(32) NOT NULL,
crs_code VARCHAR(50), -- 如 'EPSG:4326'
geo_bbox JSON, -- 存储 {minx,miny,maxx,maxy},便于空间查询
band_count TINYINT,
data_type ENUM('uint8','uint16','float32') DEFAULT 'uint16',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_md5 (md5), -- 快速去重
SPATIAL INDEX idx_geo_bbox (geo_bbox) -- MySQL 8.0+ 空间索引
);
-- 2. 拼接任务表:状态机设计
CREATE TABLE mosaic_task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
task_code VARCHAR(50) NOT NULL UNIQUE,
status ENUM('WAITING','RUNNING','SUCCESS','FAILED','TIMEOUT') DEFAULT 'WAITING',
input_files JSON NOT NULL, -- 存储文件ID数组
output_crs VARCHAR(50),
progress TINYINT DEFAULT 0,
start_time DATETIME NULL,
end_time DATETIME NULL,
output_path VARCHAR(500),
error_msg TEXT,
create_by BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_create_time (create_time)
);
为什么这样设计?
-
geo_bbox JSON字段:允许灵活存储任意坐标系下的范围,避免为每个 CRS 新建字段。配合 MySQL 8.0 的ST_Contains()函数,可高效查询“某区域内的所有影像”。 -
SPATIAL INDEX:MySQL 的空间索引对POINT、POLYGON有效,但JSON字段需配合ST_GeomFromGeoJSON()转换。实际查询示例:
sql SELECT * FROM image_file_info WHERE ST_Contains( ST_GeomFromGeoJSON(CONCAT('{"type":"Polygon","coordinates":[[[', JSON_EXTRACT(geo_bbox, '$.minx'), ',', JSON_EXTRACT(geo_bbox, '$.miny'), '], [', JSON_EXTRACT(geo_bbox, '$.maxx'), ',', JSON_EXTRACT(geo_bbox, '$.miny'), '], [', JSON_EXTRACT(geo_bbox, '$.maxx'), ',', JSON_EXTRACT(geo_bbox, '$.maxy'), '], [', JSON_EXTRACT(geo_bbox, '$.minx'), ',', JSON_EXTRACT(geo_bbox, '$.maxy'), '], [', JSON_EXTRACT(geo_bbox, '$.minx'), ',', JSON_EXTRACT(geo_bbox, '$.miny'), ']]]}')), ST_PointFromText(CONCAT('POINT(', ?, ' ', ?), 4326)) ); -
input_files JSON:存储["file_id_001","file_id_002"],而非新建关联表。理由:拼接任务生命周期短(<1小时),且关联文件数通常 ≤10,JSON 查询比 JOIN 更快。若需统计“某影像被多少任务使用”,可用SELECT COUNT(*) FROM mosaic_task WHERE JSON_CONTAINS(input_files, '"file_id_001"');。
4.4 定时任务运维:不只是“每天凌晨执行”
image-quartz 模块预置了两个实用定时任务:
-
影像过期清理:Cron 表达式
0 0 2 * * ?(每天凌晨2点),扫描image_file_info表,删除create_time超过 30 天且未被任何mosaic_task引用的文件(通过左连接mosaic_task_file判断)。清理前会生成报告邮件,列出待删文件清单。 -
拼接失败重试:Cron
0 */2 * * * ?(每2小时),扫描mosaic_task中status='FAILED'且error_msg LIKE '%timeout%'的任务,自动重置为WAITING并增加retry_count,最多重试3次。重试时会动态调整timeoutMinutes(首次30分钟,第二次45分钟,第三次60分钟),应对服务器负载波动。
注意:Quartz 的 Cron 表达式在若依后台“系统监控→定时任务”页面可直接编辑,无需重启应用。但修改后需点击“立即执行”按钮触发一次,验证逻辑是否正确——这是很多团队忽略的验证步骤。
5. 常见问题排查与独家避坑指南
部署和使用过程中,90% 的问题集中在 GDAL 环境、内存配置、路径权限三方面。以下是我在 12 个真实客户现场踩过的坑,附带可直接复用的诊断命令。
5.1 GDAL 相关问题速查表
| 问题现象 | 根本原因 | 诊断命令 | 解决方案 |
|---|---|---|---|
CRSError: Unable to lookup EPSG:4326 | GDAL_DATA 未设置或路径错误 | echo $GDAL_DATA && ls $GDAL_DATA/epsg.wkt | 确认 epsg.wkt 存在,否则重新安装 GDAL 或手动下载 epsg.wkt 放入对应目录 |
rasterio.errors.RasterioIOError: Unable to open XXX.tif | 文件权限不足或 SELinux 限制 | ls -l /path/to/file.tif && sestatus | chmod 644 file.tif;若 SELinux 启用,执行 chcon -t httpd_sys_rw_content_t /path/to/dir |
MemoryError 在 rasterio.merge.merge() | 输入影像过大,GDAL 内存映射超出系统限制 | free -h && cat /proc/meminfo \| grep MemAvailable | 减少 num_workers 至 1,或升级服务器内存;临时方案:在 img_mosaic.py 中添加 rasterio.Env(rasterio.env.EnvOptions(max_chunk_size=1024*1024)) |
| 拼接结果为全黑/全白 | src_nodata 未正确设置,导致背景被填充为 0 | gdalinfo -stats output.tif \| grep -A5 "Band 1" | 在 img_mosaic.py 的 merge() 调用中显式传入 src_nodata=0(根据实际影像调整) |
5.2 JVM 与系统级问题排查
-
问题:
java.lang.OutOfMemoryError: Compressed class space
这是元空间(Metaspace)不足,常见于频繁热部署或加载大量类。ry.sh中已设置-XX:MetaspaceSize=256m,但若你新增了 5 个以上自定义模块,需调高至512m。诊断命令:jstat -gc <pid>查看MU(Metaspace Used)是否接近MC(Metaspace Capacity)。 -
问题:
WebSocket connection closed频繁断开
通常因 Nginx 代理超时。若你用 Nginx 做反向代理,必须在location /块中添加:
nginx proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; # 关键!延长超时至24小时
否则默认 60 秒超时会强制关闭长连接。 -
问题:
image-generator生成的 HTML 页面地图不显示
检查image-admin的vue.config.js是否配置了devServer.proxy正确转发/api请求。开发时常见错误是代理指向了localhost:8080,但后端实际运行在192.168.1.100:8080,导致Leaflet的geo_bbox解析失败。解决方案:在vue.config.js中设置target: 'http://your-server-ip:8080'。
5.3 代码生成器高频问题
-
问题:生成的 Entity 类缺少
@TableField注解
若依生成器默认只对id、create_time等约定字段加注解。若你的表有crs_code字段,需在数据库列注释中写明// @TableField(value = "crs_code", jdbcType = JdbcType.VARCHAR),生成器会自动提取。 -
问题:生成的 HTML 页面提交后 404
检查image-system的pom.xml中ruoyi-framework依赖版本是否与image-generator一致。若不一致(如 framework 是 4.7.5,generator 是 4.7.0),会导致@RequiresPermissions注解解析失败,Spring Security 拦截所有请求。解决方案:统一升级至最新稳定版。
最后分享一个硬核技巧:当你需要快速验证 GDAL Python 脚本是否能在服务器运行,不必启动整个 Java 应用。进入
image-mosaic/src/main/resources目录,执行:
bash python -c " import rasterio from rasterio.crs import CRS print('GDAL Version:', rasterio.__gdal_version__) print('CRS Support:', CRS.from_epsg(4326)) "
若输出正常,则 GDAL 环境无问题;若报错,则问题一定出在环境变量或权限上,与 Java 无关。
6. 二次开发与功能扩展路径:从“能用”到“好用”
这套系统的设计哲学是:“基础能力开箱即用,专业能力插件化扩展”。这意味着你不必修改核心代码,就能安全地叠加新功能。下面是我为三个典型客户需求做的扩展方案,全部基于现有模块结构。
6.1 需求:支持 COG(Cloud Optimized GeoTIFF)格式输出
COG 是 Web 地图服务的事实标准,但 rasterio.merge.merge() 默认输出非金字塔 TIF。扩展步骤:
-
在
image-mosaic模块新增CogMosaicService:实现MosaicService接口,复用原有merge()逻辑,但输出后调用rio cogeo create命令(需提前安装rio-cogeo):
bash rio cogeo create /tmp/output.tif /tmp/output_cog.tif \ --web-optimized \ --zoom-level-strategy auto \ --resampling bilinear -
在
image-system的MosaicTaskParam中新增outputFormat字段(ENUM:TIF,COG,PNG),前端增加单选框。 -
修改
MosaicExecutor:根据outputFormat选择调用DefaultMosaicService或CogMosaicService。
优势:完全不侵入原有拼接逻辑,COG 输出作为可选增强,且
rio cogeo命令本身经过 AWS S3 优化,生成的 COG 可直接托管到对象存储供 Leaflet/Tianditu 调用。
6.2 需求:接入 Sentinel Hub API 自动下载影像
用户不想手动上传,希望输入经纬度和时间范围,系统自动从 Sentinel Hub 下载最新影像。扩展方案:
-
新建
image-sentinel模块:引入sentinelsatSDK,实现SentinelHubDownloader服务。 -
在
image-system的菜单管理中新增“影像下载”菜单,对应SentinelDownloadController,提供POST /api/sentinel/download接口,接收bbox=[minx,miny,maxx,maxy]、dateFrom、dateTo、cloudCover参数。 -
下载完成后,自动触发拼接任务:
SentinelDownloadService调用MosaicTaskService.createTask(),传入下载得到的文件路径列表。
关键点:
sentinelsat的download_all()方法会阻塞主线程,因此必须用@Async注解将其放入线程池,并设置超时(@Async("sentinelThreadPool"))。线程池需单独配置,避免占用主业务线程。
6.3 需求:拼接结果发布为 WMS 服务
让拼接好的 TIF 直接变成标准 WMS 图层,供 QGIS 或其他系统调用。方案:
-
利用
image-quartz的定时任务能力:新增一个任务,定期扫描mosaic_task表中status='SUCCESS'的记录。 -
调用 GeoServer REST API:使用
RestTemplate向本地 GeoServer(需单独部署)发送请求,自动创建 Workspace、Store、Layer。核心代码:
java String xml = "<coverage><name>" + taskCode + "</name><title>" + taskCode + "</title></coverage>"; HttpHeaders headers = new HttpHeaders(); headers.setBasicAuth("admin", "geoserver"); HttpEntity<String> entity = new HttpEntity<>(xml, headers); restTemplate.postForObject( "http://localhost:8080/geoserver/rest/workspaces/myws/coveragestores/mytiff/coverages", entity, String.class); -
将生成的 WMS URL 写回
mosaic_task.output_path字段(如http://wms.example.com/geoserver/myws/wms?service=WMS&...),前端即可展示“WMS 地址”按钮。
这种“能力编排”思维,正是若依模块化设计的价值所在——你永远在组合积木,而非重造轮子。
我个人在实际操作中的体会是:这套系统最强大的地方,不在于它能拼接多少景影像,而在于它把遥感处理中那些琐碎却致命的细节(坐标系、NoData、内存、权限、日志)全部封装成了可配置、可监控、可审计的标准化能力。当你把精力从“怎么让 GDAL 不报错”转移到“怎么设计一个更好的影像质量评估算法”时,才是真正进入了专业领域的深水区。
简介:一套开箱即用的遥感影像Web处理工具,专注TIF格式遥感图的上传、自动镶嵌拼接与下载。后端基于若依(RuoYi)框架构建,内置用户权限管理、部门/岗位/角色配置、菜单与字典维护、系统参数设置等基础运维能力;支持操作日志记录、登录行为审计、在线用户实时监控、定时任务调度等功能。配套提供SQL初始化脚本(image_mosaic.sql)、Windows启动脚本(ry.bat、run.bat)、Linux部署脚本(ry.sh),以及环境说明和详细README文档。源码按模块清晰划分:image-system负责核心业务逻辑,image-quartz支撑定时任务,image-generator集成可视化代码生成器,可一键导出Java后端、HTML前端、XML配置及SQL建表语句。所有模块均适配Windows与Linux双环境,结构规范、注释完整,便于二次开发与功能扩展。
&spm=1001.2101.3001.5002&articleId=162158303&d=1&t=3&u=31b64a6947af4abf8a63ed7de70275ab)

被折叠的 条评论
为什么被折叠?



