From 1f6c81158b79f75551258649f340f1b0d1949b0e Mon Sep 17 00:00:00 2001 From: duxin Date: Tue, 7 Jul 2026 10:01:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8A=A8=E6=80=81=E6=9F=A5=E6=89=BE=20P?= =?UTF-8?q?ROJ/GDAL=20=E8=B7=AF=E5=BE=84=20+=20Warp=201=20=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E6=B5=AE=E7=82=B9=E8=88=8D=E5=85=A5=E5=AE=B9=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **water_quality_gui_v2.py:** - 废弃硬编码 conda_env 路径 - 新增 _find_and_set_gdal_env(): 基于 sys.prefix 自动推断 Library/share/proj 和 Library/share/gdal,验证 proj.db 存在后 强设 PROJ_LIB 和 GDAL_DATA 环境变量 - 兼容 Windows (Library/share/...) 和 Linux/Mac (share/...) 路径 **preview_generator.py _warp_mask_to_image():** - 尺寸验证从严格相等改为 ±2 像素容差 - 新增 _snap_array_to_shape(): warp 输出与 target_shape 相差 ≤2px 时自动 slice(截断)或 pad(填充),不再回退到盲读模式 - 解决坐标系转换浮点舍入导致的 1048x522 vs 1047x521 问题 --- src/core/utils/preview_generator.py | 70 +++++++++++++++++++++++++++-- src/gui/water_quality_gui_v2.py | 58 ++++++++++++++++++++---- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/src/core/utils/preview_generator.py b/src/core/utils/preview_generator.py index eb6e5ad..29dd826 100644 --- a/src/core/utils/preview_generator.py +++ b/src/core/utils/preview_generator.py @@ -266,23 +266,87 @@ def _warp_mask_to_image(mask_path: str, mask_ds = None # 源掩膜已不再需要 - # ---- 验证输出尺寸 ---- + # ---- 验证输出尺寸(容忍 ±WARP_SIZE_TOLERANCE 像素的浮点舍入) ---- + # 坐标系转换时的浮点舍入可能导致 warp 输出与底图相差 1-2 像素, + # 此时地理位置已经对齐,不应回退到直接读取模式。 + _WARP_SIZE_TOLERANCE = 2 warped_w = warped_ds.RasterXSize warped_h = warped_ds.RasterYSize - if warped_w != base_w or warped_h != base_h: + dw = warped_w - base_w + dh = warped_h - base_h + + if abs(dw) > _WARP_SIZE_TOLERANCE or abs(dh) > _WARP_SIZE_TOLERANCE: warped_ds = None raise RuntimeError( f"gdal.Warp 输出尺寸 ({warped_w}x{warped_h}) 与底图 " - f"({base_w}x{base_h}) 不一致!请检查投影/分辨率参数。" + f"({base_w}x{base_h}) 差异过大 (dw={dw}, dh={dh})!" + f"请检查投影/分辨率参数。" ) # ---- 读取 warped 数据 ---- warped_data = warped_ds.GetRasterBand(1).ReadAsArray().astype(np.float32) warped_ds = None + # ---- 微调尺寸到 target_shape(slice 多余边缘 或 pad 缺失边缘) ---- + if dw != 0 or dh != 0: + warped_data = _snap_array_to_shape( + warped_data, base_h, base_w, fill_value=nodata_output + ) + return warped_data, nodata_output +def _snap_array_to_shape(arr: np.ndarray, + target_h: int, + target_w: int, + fill_value: float = _WARP_NODATA) -> np.ndarray: + """将数组尺寸微调到目标 shape(±2 像素容差内) + + 处理坐标系转换时浮点舍入导致的 1-2 像素差异。 + - 数组比目标大 → 从边缘截断多余行/列 + - 数组比目标小 → 用 fill_value 填充缺失的行/列 + - 因 warp 已确保地理对齐,多出的边缘像素为投影边界舍入产物 + + Args: + arr: 输入数组,shape = (h, w) + target_h: 目标高度(行数) + target_w: 目标宽度(列数) + fill_value: 填充值(默认 _WARP_NODATA = -9999.0) + + Returns: + 调整后的数组,shape 精确为 (target_h, target_w) + """ + h, w = arr.shape + dh = h - target_h + dw = w - target_w + + if dh == 0 and dw == 0: + return arr + + # --- 裁剪多余行/列(优先从末尾截断) --- + if dh > 0: + arr = arr[:target_h, :] + if dw > 0: + arr = arr[:, :target_w] + + # --- 填充缺失行/列 --- + if dh < 0: + pad_bottom = abs(dh) + arr = np.pad(arr, ((0, pad_bottom), (0, 0)), mode='constant', + constant_values=fill_value) + if dw < 0: + pad_right = abs(dw) + arr = np.pad(arr, ((0, 0), (0, pad_right)), mode='constant', + constant_values=fill_value) + + print(f"[尺寸微调] warp 输出 ({h}x{w}) → 目标 ({target_h}x{target_w})," + f"已{'裁剪' if dh > 0 or dw > 0 else ''}" + f"{'填充' if dh < 0 or dw < 0 else ''}" + f"{'/' if dh > 0 or dw > 0 and (dh < 0 or dw < 0) else ''}") + + return arr + + # ============================================================ # 核心预览图生成函数 # ============================================================ diff --git a/src/gui/water_quality_gui_v2.py b/src/gui/water_quality_gui_v2.py index 8aeaaa8..3c6f7a9 100644 --- a/src/gui/water_quality_gui_v2.py +++ b/src/gui/water_quality_gui_v2.py @@ -17,15 +17,57 @@ WaterQualityGUI 只负责窗口框架(标题栏、菜单栏、状态栏、QTab """ import os - -# 强行切断系统环境变量的干扰,绑定到当前的 Conda 虚拟环境 -# 必须在 import gdal / import rasterio 等任何第三方库之前执行 -conda_env = r"D:\111\changyongruanjian\anconda\envs\WQ_GUI" - -os.environ["PROJ_LIB"] = os.path.join(conda_env, "Library", "share", "proj") -os.environ["GDAL_DATA"] = os.path.join(conda_env, "Library", "share", "gdal") - import sys + +# ═══════════════════════════════════════════════════════════════ +# 动态推断当前 Conda 环境下的 GDAL/PROJ 数据路径 +# 必须在 import gdal / import rasterio 等任何第三方库之前执行 +# ═══════════════════════════════════════════════════════════════ + +def _find_and_set_gdal_env(): + """根据 sys.prefix 自动推断并强设 PROJ_LIB 和 GDAL_DATA 环境变量""" + # sys.prefix 在 Conda 环境下通常为: + # D:\...\anconda\envs\WQ_GUI + # PROJ 和 GDAL 的数据文件位于: + # {sys.prefix}\Library\share\proj (含 proj.db) + # {sys.prefix}\Library\share\gdal + prefix = sys.prefix + candidates_proj = [ + os.path.join(prefix, "Library", "share", "proj"), # Windows Conda + os.path.join(prefix, "share", "proj"), # Linux/Mac Conda + ] + candidates_gdal = [ + os.path.join(prefix, "Library", "share", "gdal"), + os.path.join(prefix, "share", "gdal"), + ] + + proj_lib = None + for path in candidates_proj: + proj_db = os.path.join(path, "proj.db") + if os.path.isfile(proj_db): + proj_lib = path + break + + gdal_data = None + for path in candidates_gdal: + if os.path.isdir(path): + gdal_data = path + break + + if proj_lib: + os.environ["PROJ_LIB"] = proj_lib + print(f"[ENV] PROJ_LIB → {proj_lib}") + else: + print("[ENV] ⚠ 未找到 proj.db,PROJ_LIB 保持系统默认,可能影响坐标转换") + + if gdal_data: + os.environ["GDAL_DATA"] = gdal_data + print(f"[ENV] GDAL_DATA → {gdal_data}") + else: + print("[ENV] ⚠ 未找到 GDAL data 目录,GDAL_DATA 保持系统默认") + +_find_and_set_gdal_env() + import ctypes import traceback import multiprocessing