fix: 多项稳定性修复与打包优化
- PyInstaller: runtime hook 预加载 osgeo DLL 避免 Qt5 符号冲突; spec 纳入 osgeo .py 文件 + 运行时 DLL 依赖 (ffi/lzma/bz2/expat/sqlite3); 移除 DLL 双副本防止 segfault - GDAL 环境: 新增 _MEIPASS/osgeo/data/gdal 路径搜索,改善打包后 GDAL_DATA 检测 - 预览生成器: 掩膜与底图同分辨率时跳过 Warp 加速读取 - 专题图: ConvexHull 坐标中心化修复 UTM 大坐标精度退化; 边界采样兜底防止外扩点为空; TIFF 已含 NaN 掩膜时跳过矢量擦除; 形态学闭运算填充掩膜小孔洞 - NDWI: int16→float32 防止减法溢出 - 面板注册表: 步骤模块分组重构 (模块一/二/三/四重划分)
This commit is contained in:
@ -17,9 +17,24 @@ def _safe_add(path: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# PyInstaller onefile 解包目录
|
||||
# PyInstaller onedir 解包目录
|
||||
base = getattr(sys, "_MEIPASS", None)
|
||||
if base:
|
||||
_safe_add(base)
|
||||
_safe_add(os.path.join(base, "lib-dynload"))
|
||||
_safe_add(os.path.join(base, "DLLs"))
|
||||
_safe_add(os.path.join(base, "DLLs"))
|
||||
# osgeo 子目录包含 gdal.dll / geos.dll / proj_9.dll 等 GIS 核心库
|
||||
osgeo_dir = os.path.join(base, "osgeo")
|
||||
_safe_add(osgeo_dir)
|
||||
|
||||
# ★ 关键:在 PyQt5 之前预加载 osgeo 的所有 DLL
|
||||
# 避免 Qt5 DLL 加载后 osgeo DLL 加载时发生符号冲突
|
||||
if os.path.isdir(osgeo_dir):
|
||||
import ctypes as _ct
|
||||
for _f in os.listdir(osgeo_dir):
|
||||
if _f.lower().endswith('.dll'):
|
||||
_dll_path = os.path.join(osgeo_dir, _f)
|
||||
try:
|
||||
_ct.CDLL(_dll_path)
|
||||
except Exception:
|
||||
pass
|
||||
@ -215,19 +215,29 @@ def _warp_mask_to_image(mask_path: str,
|
||||
minx, maxx = min(corners_x), max(corners_x)
|
||||
miny, maxy = min(corners_y), max(corners_y)
|
||||
|
||||
# ---- 读取掩膜的原始 NoData 并传递到 Warp ----
|
||||
# ---- 读取掩膜 ----
|
||||
mask_ds = gdal.Open(mask_path, gdal.GA_ReadOnly)
|
||||
if mask_ds is None:
|
||||
raise ValueError(f"无法打开掩膜文件: {mask_path}")
|
||||
|
||||
src_nodata = None
|
||||
mask_gt = mask_ds.GetGeoTransform()
|
||||
mask_proj = mask_ds.GetProjection()
|
||||
try:
|
||||
src_nodata = mask_ds.GetRasterBand(1).GetNoDataValue()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 快速通道:掩膜与底图同分辨率同范围时跳过 Warp ──
|
||||
mask_gt = mask_ds.GetGeoTransform()
|
||||
mask_w, mask_h = mask_ds.RasterXSize, mask_ds.RasterYSize
|
||||
if (mask_w == base_w and mask_h == base_h
|
||||
and abs(mask_gt[1] - base_gt[1]) < 1e-9
|
||||
and abs(mask_gt[5] - base_gt[5]) < 1e-9):
|
||||
data = mask_ds.GetRasterBand(1).ReadAsArray().astype(np.float32)
|
||||
mask_ds = None
|
||||
print("[预览] 掩膜与底图分辨率一致,跳过 Warp 直接读取")
|
||||
return _normalize_mask(data, nodata_value=src_nodata), nodata_output
|
||||
mask_proj = mask_ds.GetProjection()
|
||||
|
||||
# ---- 构建 Warp 选项 ----
|
||||
warp_kwargs = {
|
||||
'format': 'MEM',
|
||||
|
||||
@ -74,16 +74,12 @@ PANEL_REGISTRY = [
|
||||
},
|
||||
'constructor_kwargs': None,
|
||||
},
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 模块二 特征工程与数据
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
'step_id': 'step4_sampling',
|
||||
'class_ref': Step4SamplingPanel,
|
||||
'title': '采样点布设',
|
||||
'icon': '4.png',
|
||||
'stage': '模块二 特征工程与数据',
|
||||
'stage': '模块一 影像预处理',
|
||||
'display_name': '4. 采样点布设',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名;
|
||||
# 第三元素 source_attr = 上游源控件真实属性名(panel_factory 回放端使用)
|
||||
@ -95,12 +91,15 @@ PANEL_REGISTRY = [
|
||||
},
|
||||
'constructor_kwargs': None,
|
||||
},
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 模块二 光谱建模与反演
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
'step_id': 'step5_clean',
|
||||
'class_ref': Step5CleanPanel,
|
||||
'title': '数据清洗',
|
||||
'icon': '5.png',
|
||||
'stage': '模块二 特征工程与数据',
|
||||
'stage': '模块二 光谱建模与反演',
|
||||
'display_name': '5. 数据清洗',
|
||||
# 业务要求保持输入源独立,不自动抓取 step4_sampling 的输出
|
||||
'dependencies': None,
|
||||
@ -111,7 +110,7 @@ PANEL_REGISTRY = [
|
||||
'class_ref': Step6FeaturePanel,
|
||||
'title': '光谱特征',
|
||||
'icon': '6.png',
|
||||
'stage': '模块二 特征工程与数据',
|
||||
'stage': '模块二 光谱建模与反演',
|
||||
'display_name': '6. 光谱特征提取',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名
|
||||
'dependencies': {
|
||||
@ -131,7 +130,7 @@ PANEL_REGISTRY = [
|
||||
'class_ref': Step7InversionPanel,
|
||||
'title': '水质光谱指数计算',
|
||||
'icon': '7.png',
|
||||
'stage': '模块二 特征工程与数据',
|
||||
'stage': '模块二 光谱建模与反演',
|
||||
'display_name': '7. 水质指数计算',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名;
|
||||
# 第三元素 source_attr = 上游源控件真实属性名
|
||||
@ -142,15 +141,12 @@ PANEL_REGISTRY = [
|
||||
'constructor_kwargs': None,
|
||||
},
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 模块三 模型训练与反演
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
'step_id': 'step8_ml_train',
|
||||
'class_ref': Step8MlTrainPanel,
|
||||
'title': '机器学习建模',
|
||||
'icon': '8.png',
|
||||
'stage': '模块三 模型训练与反演',
|
||||
'stage': '模块二 光谱建模与反演',
|
||||
'display_name': '8. 机器学习建模',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名
|
||||
'dependencies': {
|
||||
@ -160,15 +156,12 @@ PANEL_REGISTRY = [
|
||||
'constructor_kwargs': None,
|
||||
},
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 模块三 模型训练与反演(续)→ 模块四 制图与成果汇编
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
'step_id': 'step9_ml_predict',
|
||||
'class_ref': Step9MlPredictPanel,
|
||||
'title': '机器学习预测',
|
||||
'icon': '9.png',
|
||||
'stage': '模块三 模型训练与反演',
|
||||
'stage': '模块二 光谱建模与反演',
|
||||
'display_name': '9. 机器学习预测',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名;
|
||||
# 第三元素 source_attr = 上游源控件真实属性名
|
||||
@ -178,12 +171,16 @@ PANEL_REGISTRY = [
|
||||
},
|
||||
'constructor_kwargs': None,
|
||||
},
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 模块三 物理公式反演
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
'step_id': 'step10_watercolor',
|
||||
'class_ref': Step10WatercolorPanel,
|
||||
'title': '水色指数反演',
|
||||
'icon': '10.png',
|
||||
'stage': '模块三 模型训练与反演',
|
||||
'stage': '模块三 物理公式反演',
|
||||
'display_name': '10. 水色指数反演',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名;
|
||||
# 第三元素 source_attr = 上游源控件真实属性名
|
||||
@ -194,12 +191,16 @@ PANEL_REGISTRY = [
|
||||
},
|
||||
'constructor_kwargs': None,
|
||||
},
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 模块四 制图与成果输出
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
'step_id': 'step11_map',
|
||||
'class_ref': Step11MapPanel,
|
||||
'title': '专题图生成',
|
||||
'icon': '11.png',
|
||||
'stage': '模块四 制图与成果汇编',
|
||||
'stage': '模块四 制图与成果输出',
|
||||
'display_name': '11. 分布图生成',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名
|
||||
'dependencies': {
|
||||
@ -217,7 +218,7 @@ PANEL_REGISTRY = [
|
||||
'class_ref': Step12VizPanel,
|
||||
'title': '可视化',
|
||||
'icon': '9.png',
|
||||
'stage': '模块四 制图与成果汇编',
|
||||
'stage': '模块四 制图与成果输出',
|
||||
'display_name': '12. 可视化展示',
|
||||
'dependencies': None,
|
||||
'constructor_kwargs': None,
|
||||
@ -227,7 +228,7 @@ PANEL_REGISTRY = [
|
||||
'class_ref': Step13ReportPanel,
|
||||
'title': '报告生成',
|
||||
'icon': '13.png',
|
||||
'stage': '模块四 制图与成果汇编',
|
||||
'stage': '模块四 制图与成果输出',
|
||||
'display_name': '13. 分析报告生成',
|
||||
'dependencies': None,
|
||||
'constructor_kwargs': {'main_window'}, # 需要注入 main_window=self
|
||||
|
||||
@ -78,9 +78,12 @@ def _find_and_set_gdal_env():
|
||||
return None
|
||||
|
||||
def _find_gdal_in_prefix(prefix):
|
||||
"""在指定前缀下查找 Library/share/gdal"""
|
||||
"""在指定前缀下查找 GDAL data 目录(Library/share/gdal 或 osgeo/data/gdal)"""
|
||||
# Conda 路径:Library/share/gdal
|
||||
# pip wheel / PyInstaller 打包后:osgeo/data/gdal
|
||||
for sub in ("Library/share/gdal", "share/gdal",
|
||||
"Library\\share\\gdal", "share\\gdal"):
|
||||
"Library\\share\\gdal", "share\\gdal",
|
||||
"osgeo/data/gdal", "osgeo\\data\\gdal"):
|
||||
path = os.path.join(prefix, sub)
|
||||
if os.path.isdir(path):
|
||||
for marker in ("gdalvrt.xsd", "pcs.csv", "gcs.csv"):
|
||||
@ -110,6 +113,12 @@ def _find_and_set_gdal_env():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2b) PyInstaller 打包后的解包目录 (_MEIPASS)
|
||||
# sys.prefix 在 PyInstaller 中已指向 _MEIPASS,但显式加入确保不被遗漏
|
||||
_meipass = getattr(sys, '_MEIPASS', None)
|
||||
if _meipass and _meipass not in prefixes:
|
||||
prefixes.insert(0, _meipass)
|
||||
|
||||
# 3) 从 sys.path 中 site-packages 位置回推所有可能的 conda/env 前缀
|
||||
try:
|
||||
for sp in sys.path:
|
||||
@ -236,10 +245,10 @@ def _find_and_set_gdal_env():
|
||||
# 不设置的话 Fiona 会使用自带的旧版 proj.db (VERSION.MINOR=2),
|
||||
# 导致投影解析错误 → 矢量图层错位 → 水体被错误擦除 → 有效像元为 0
|
||||
os.environ["PROJ_DATA"] = proj_lib
|
||||
print(f"[ENV] PROJ_LIB → {proj_lib}")
|
||||
print(f"[ENV] PROJ_DATA → {proj_lib}")
|
||||
print(f"[ENV] PROJ_LIB -> {proj_lib}")
|
||||
print(f"[ENV] PROJ_DATA -> {proj_lib}")
|
||||
else:
|
||||
print("[ENV] ⚠ 未找到兼容的 proj.db (需要 PROJ v6+),PROJ_LIB/PROJ_DATA 保持系统默认")
|
||||
print("[ENV] [WARN] 未找到兼容的 proj.db (需要 PROJ v6+),PROJ_LIB/PROJ_DATA 保持系统默认")
|
||||
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀: {[os.path.basename(p) for p in prefixes[:5]]}...)")
|
||||
print("[ENV] 如果 step11 地图渲染报 PROJ 版本错误,")
|
||||
print("[ENV] 请手动设置: set PROJ_LIB=<conda环境>\\Library\\share\\proj")
|
||||
@ -247,9 +256,9 @@ def _find_and_set_gdal_env():
|
||||
|
||||
if gdal_data:
|
||||
os.environ["GDAL_DATA"] = gdal_data
|
||||
print(f"[ENV] GDAL_DATA → {gdal_data}")
|
||||
print(f"[ENV] GDAL_DATA -> {gdal_data}")
|
||||
else:
|
||||
print("[ENV] ⚠ 未找到 GDAL data 目录,GDAL_DATA 保持系统默认")
|
||||
print("[ENV] [WARN] 未找到 GDAL data 目录,GDAL_DATA 保持系统默认")
|
||||
print(f"[ENV] (已搜索 {len(prefixes)} 个前缀)")
|
||||
|
||||
# ── 屏蔽 Fiona 内部旧版 PROJ 路径 ──
|
||||
@ -260,7 +269,7 @@ def _find_and_set_gdal_env():
|
||||
if "PROJ_DATA" in os.environ:
|
||||
print("[ENV] 已屏蔽 Fiona 内部旧版 PROJ 数据库,全进程统一使用上述 proj.db")
|
||||
if _fiona_v2_path:
|
||||
print(f"[ENV] ⚠ 检测到 Fiona 自带的旧版 proj.db (v2) 位于:")
|
||||
print(f"[ENV] [WARN] 检测到 Fiona 自带的旧版 proj.db (v2) 位于:")
|
||||
print(f" {_fiona_v2_path}")
|
||||
print(f" PROJ_DATA 已指向 v6+ 版本,此旧版将被忽略")
|
||||
if proj_lib:
|
||||
|
||||
@ -1082,8 +1082,10 @@ class ContentMapper:
|
||||
return np.array([])
|
||||
|
||||
try:
|
||||
# 使用凸包识别边缘点
|
||||
hull = ConvexHull(points)
|
||||
# 坐标中心化 + QJ joggle,解决 UTM 大坐标(60万+)精度退化问题
|
||||
center = points.mean(axis=0)
|
||||
points_centered = points - center
|
||||
hull = ConvexHull(points_centered, qhull_options='QJ')
|
||||
edge_indices = hull.vertices
|
||||
|
||||
print(f"识别到 {len(edge_indices)} 个边缘采样点(共 {len(points)} 个点)")
|
||||
@ -1274,6 +1276,63 @@ class ContentMapper:
|
||||
|
||||
return result_gdf
|
||||
else:
|
||||
# ── 兜底:中心射线法失败时,用水域边界均匀采样作为外扩点 ──
|
||||
if boundary_gdf is not None:
|
||||
print("中心射线法未生成外扩点,改用边界采样兜底...")
|
||||
try:
|
||||
# 提取水域边界线(所有多边形的 exterior + interiors)
|
||||
boundary_lines = []
|
||||
for geom in boundary_gdf.geometry:
|
||||
if hasattr(geom, 'exterior'):
|
||||
boundary_lines.append(geom.exterior)
|
||||
boundary_lines.extend(geom.interiors)
|
||||
if boundary_lines:
|
||||
# 沿边界采样:间距 = max(resolution*5, 水体宽度的1/20)
|
||||
# 限制总数 ≤500,避免插值爆内存
|
||||
_sample_dist = max(resolution * 5,
|
||||
min(line.length for line in boundary_lines) / 10)
|
||||
boundary_pts = []
|
||||
_max_pts = 500
|
||||
_pts_per_line = max(2, _max_pts // max(1, len(boundary_lines)))
|
||||
for line in boundary_lines:
|
||||
length = line.length
|
||||
n = min(_pts_per_line, max(2, int(length / _sample_dist) + 1))
|
||||
for i in range(n):
|
||||
pt = line.interpolate(i * length / max(1, n - 1), normalized=True)
|
||||
boundary_pts.append((pt.x, pt.y))
|
||||
if boundary_pts:
|
||||
boundary_pts = np.unique(np.array(boundary_pts), axis=0)
|
||||
# 去重:只保留离原始点 > 2×分辨率的边界点
|
||||
if len(points) > 0 and len(boundary_pts) > 0:
|
||||
from scipy.spatial import cKDTree
|
||||
tree = cKDTree(points)
|
||||
dists, _ = tree.query(boundary_pts, distance_upper_bound=resolution*2)
|
||||
far_mask = ~np.isfinite(dists) | (dists > resolution)
|
||||
boundary_pts = boundary_pts[far_mask]
|
||||
boundary_pts = boundary_pts[:_max_pts] # 硬截断
|
||||
if len(boundary_pts) > 0:
|
||||
# 用最近邻采样点的真实值(而非均值),防止 IDW 被均值污染
|
||||
_tree = cKDTree(points)
|
||||
_, _nn_idx = _tree.query(boundary_pts, k=1)
|
||||
_nn_vals = points_gdf['content'].values[_nn_idx]
|
||||
new_rows = []
|
||||
for i, bp in enumerate(boundary_pts):
|
||||
new_rows.append({
|
||||
'proj_x': float(bp[0]), 'proj_y': float(bp[1]),
|
||||
'longitude': float(bp[0]), 'latitude': float(bp[1]),
|
||||
'content': float(_nn_vals[i]),
|
||||
'geometry': Point(float(bp[0]), float(bp[1]))
|
||||
})
|
||||
expanded_gdf = gpd.GeoDataFrame(new_rows, crs=points_gdf.crs)
|
||||
result_gdf = gpd.GeoDataFrame(
|
||||
pd.concat([points_gdf, expanded_gdf], ignore_index=True),
|
||||
crs=points_gdf.crs
|
||||
)
|
||||
print(f"边界采样兜底:新增 {len(boundary_pts)} 个边界点")
|
||||
return result_gdf
|
||||
except Exception as e:
|
||||
print(f"边界采样兜底也失败: {e}")
|
||||
|
||||
print("未生成外扩点,返回原始点集")
|
||||
return points_gdf.copy()
|
||||
|
||||
@ -2566,11 +2625,14 @@ class ContentMapper:
|
||||
array
|
||||
)
|
||||
|
||||
# ====== 新增:矢量掩膜物理擦除(必须在降采样之前,否则 array.shape 与 transform 错位)======
|
||||
# 把水域多边形外部的陆地像素物理擦除为 NaN,让下游 mean/std 统计 100% 干净(无陆地假数据污染)
|
||||
# 同时保留 boundary_gdf_plotted 给末尾描边复用,避免重复读取 + 重复矢量化
|
||||
# ====== 矢量掩膜物理擦除 ======
|
||||
# 如果 TIFF 本身已有 NaN 掩膜(Step 11 栅格裁剪过),跳过重复擦除以避免黑点
|
||||
boundary_gdf_plotted: Optional[Any] = None
|
||||
if boundary_shp_path and os.path.isfile(boundary_shp_path) and transform is not None:
|
||||
_tif_already_masked = bool(np.any(np.isnan(array)))
|
||||
if _tif_already_masked:
|
||||
print(f"[visualize_raster] TIFF 已含 NaN 掩膜,跳过矢量擦除 "
|
||||
f"(有效像元: {int((~np.isnan(array)).sum())}/{array.size})")
|
||||
if not _tif_already_masked and boundary_shp_path and os.path.isfile(boundary_shp_path) and transform is not None:
|
||||
try:
|
||||
boundary_ext = Path(boundary_shp_path).suffix.lower()
|
||||
if boundary_ext in ('.shp',):
|
||||
@ -2642,6 +2704,9 @@ class ContentMapper:
|
||||
transform=transform,
|
||||
invert=True,
|
||||
)
|
||||
# 膨胀掩膜 1px:消除相邻多边形边界的 1-2px 缝隙
|
||||
from scipy.ndimage import binary_dilation
|
||||
geom_mask = binary_dilation(geom_mask, iterations=1)
|
||||
array = np.where(geom_mask, array, np.nan)
|
||||
kept = int((~np.isnan(array)).sum())
|
||||
print(f"[visualize_raster] 矢量掩膜物理擦除完成: 陆地背景 → NaN "
|
||||
@ -3214,6 +3279,10 @@ class ContentMapper:
|
||||
grid_content = self._perform_interpolation(pts, vals, grid_xx, grid_yy)
|
||||
# ⑥ 复用共享掩膜裁剪
|
||||
if mask is not None:
|
||||
# 形态学闭运算填充掩膜小孔洞(NDWI 误判的孤立非水体像素)
|
||||
from scipy.ndimage import binary_closing, generate_binary_structure
|
||||
_se = generate_binary_structure(2, 1) # 3×3 十字结构
|
||||
mask = binary_closing(mask, structure=_se, iterations=2)
|
||||
grid_content[~mask] = np.nan
|
||||
# 边界内 NaN 填充
|
||||
nan_mask = np.isnan(grid_content)
|
||||
|
||||
@ -147,9 +147,9 @@ def calculate_NDWI(green_bandnumber, nir_bandnumber, filename):
|
||||
im_proj = dataset.GetProjection() # 地图投影信息
|
||||
|
||||
tmp = dataset.GetRasterBand(green_bandnumber + 1) # 波段计数从1开始
|
||||
band_green = tmp.ReadAsArray().astype(np.int16)
|
||||
band_green = tmp.ReadAsArray().astype(np.float32)
|
||||
tmp = dataset.GetRasterBand(nir_bandnumber + 1) # 波段计数从1开始
|
||||
band_nir = tmp.ReadAsArray().astype(np.int16)
|
||||
band_nir = tmp.ReadAsArray().astype(np.float32)
|
||||
|
||||
ndwi = (band_green - band_nir) / (band_green + band_nir)
|
||||
|
||||
|
||||
@ -60,6 +60,16 @@ def get_data_files():
|
||||
if os.path.exists(rthook_script):
|
||||
datas.append((rthook_script, "scripts"))
|
||||
|
||||
# 1-6. osgeo Python 模块(__init__.py, gdal.py, ogr.py 等)
|
||||
# ★ 关键:PyInstaller 会把 .py 文件打入 PYZ (嵌入 EXE),但 .pyd 文件
|
||||
# 作为 binaries 被放到 _internal/osgeo/ 物理目录下。当物理 osgeo/
|
||||
# 目录存在但没有 __init__.py 时,Python 无法将其识别为 package,
|
||||
# 导致 "import osgeo" 失败 → "GDAL 未安装"。
|
||||
# 解决方案:将 osgeo 的所有 .py 文件显式复制到物理 osgeo/ 目录。
|
||||
if os.path.isdir(CONDA_OSGEO_DLLS):
|
||||
for py_file in glob.glob(os.path.join(CONDA_OSGEO_DLLS, "*.py")):
|
||||
datas.append((py_file, "osgeo"))
|
||||
|
||||
return datas
|
||||
|
||||
datas = get_data_files()
|
||||
@ -90,13 +100,26 @@ def get_binaries():
|
||||
if os.path.exists(dll_path):
|
||||
binaries.append((dll_path, "."))
|
||||
|
||||
# 2-2. 搜集底层 GDAL/GEOS/PROJ 核心框架与二进制扩展
|
||||
if os.path.isdir(CONDA_OSGEO_DLLS):
|
||||
for dll_name in ["gdal.dll", "geos.dll", "geos_c.dll", "proj_9.dll"]:
|
||||
dll_path = os.path.join(CONDA_OSGEO_DLLS, dll_name)
|
||||
# 2-1b. Python C 扩展的运行时依赖 DLL(_ctypes → ffi.dll, _lzma → liblzma.dll 等)
|
||||
# 这些 DLL 位于 Conda 环境的 Library/bin,PyInstaller 分析时无法自动发现
|
||||
CONDA_LIBRARY_BIN = os.path.join(CONDA_ENV_DLLS, "Library", "bin")
|
||||
if os.path.isdir(CONDA_LIBRARY_BIN):
|
||||
for dll_name in [
|
||||
"ffi.dll", # _ctypes.pyd 依赖
|
||||
"liblzma.dll", # _lzma.pyd 依赖
|
||||
"libbz2.dll", # _bz2.pyd 依赖
|
||||
"libexpat.dll", # pyexpat.pyd 依赖
|
||||
"sqlite3.dll", # _sqlite3.pyd 依赖
|
||||
]:
|
||||
dll_path = os.path.join(CONDA_LIBRARY_BIN, dll_name)
|
||||
if os.path.exists(dll_path):
|
||||
binaries.append((dll_path, "."))
|
||||
|
||||
# 2-2. 搜集底层 GDAL/GEOS/PROJ 核心框架与二进制扩展
|
||||
# ★ 注意:gdal.dll / geos.dll / geos_c.dll / proj_9.dll 不再显式添加,
|
||||
# PyInstaller 会自动将它们收集到 osgeo/ 目录(随 .pyd 依赖解析)。
|
||||
# 显式添加到根目录会导致 DLL 双副本冲突 → segfault。
|
||||
if os.path.isdir(CONDA_OSGEO_DLLS):
|
||||
# 映射 Python 3.12 对应的底层二进制接口 (C-Extension)
|
||||
for pyd_name in [
|
||||
"_gdal.cp312-win_amd64.pyd",
|
||||
@ -185,6 +208,10 @@ a.binaries = [
|
||||
if not x[0].lower().startswith(('msvcp140', 'vcruntime140'))
|
||||
]
|
||||
|
||||
# ★ 注意:.libs 目录中的 PROJ/GEOS/GDAL DLL 副本无需删除。
|
||||
# rthook_add_dll_dirs.py 运行时钩子会在 PyQt5 之前预加载 osgeo/ 的所有 DLL,
|
||||
# 避免了 DLL 加载顺序导致的 segfault。各包的 .libs 副本可以安全共存。
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
# 生成物理启动核心
|
||||
@ -198,7 +225,7 @@ exe = EXE(
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False, # 禁用 UPX 暴力压缩,保护二进制 C++ 区段完整性
|
||||
console=False, # ⚠️ 维持开启控制台,拒绝绝对静默崩溃
|
||||
console=False, # GUI 应用,不显示控制台
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
Reference in New Issue
Block a user