|
@@ -65,11 +65,14 @@ class MappingUtils:
|
|
|
self.logger = logging.getLogger(self.__class__.__name__)
|
|
|
self.logger.setLevel(log_level)
|
|
|
|
|
|
+ # 避免重复添加处理器,并防止日志传播到父级处理器导致重复输出
|
|
|
if not self.logger.handlers:
|
|
|
handler = logging.StreamHandler()
|
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
|
handler.setFormatter(formatter)
|
|
|
self.logger.addHandler(handler)
|
|
|
+ # 关闭日志传播,避免与全局basicConfig冲突
|
|
|
+ self.logger.propagate = False
|
|
|
|
|
|
def csv_to_shapefile(self, csv_file, shapefile_output, lon_col=0, lat_col=1, value_col=2):
|
|
|
"""
|
|
@@ -121,6 +124,50 @@ class MappingUtils:
|
|
|
self.logger.error(f"CSV转Shapefile失败: {str(e)}")
|
|
|
raise
|
|
|
|
|
|
+ def dataframe_to_geodataframe(self, df, lon_col=0, lat_col=1, value_col=2, field_name='Prediction'):
|
|
|
+ """
|
|
|
+ 将DataFrame直接转换为GeoDataFrame(内存处理)
|
|
|
+
|
|
|
+ @param df: pandas DataFrame
|
|
|
+ @param lon_col: 经度列索引或列名,默认第0列
|
|
|
+ @param lat_col: 纬度列索引或列名,默认第1列
|
|
|
+ @param value_col: 数值列索引或列名,默认第2列
|
|
|
+ @param field_name: 值字段名称
|
|
|
+ @return: GeoDataFrame
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ self.logger.info("开始将DataFrame转换为GeoDataFrame(内存处理)")
|
|
|
+
|
|
|
+ # 支持列索引或列名
|
|
|
+ if isinstance(lon_col, int):
|
|
|
+ lon = df.iloc[:, lon_col]
|
|
|
+ else:
|
|
|
+ lon = df[lon_col]
|
|
|
+
|
|
|
+ if isinstance(lat_col, int):
|
|
|
+ lat = df.iloc[:, lat_col]
|
|
|
+ else:
|
|
|
+ lat = df[lat_col]
|
|
|
+
|
|
|
+ if isinstance(value_col, int):
|
|
|
+ val = df.iloc[:, value_col]
|
|
|
+ else:
|
|
|
+ val = df[value_col]
|
|
|
+
|
|
|
+ # 创建几何对象
|
|
|
+ geometry = [Point(xy) for xy in zip(lon, lat)]
|
|
|
+
|
|
|
+ # 创建新的DataFrame,只包含必要的列
|
|
|
+ data = {field_name: val}
|
|
|
+ gdf = gpd.GeoDataFrame(data, geometry=geometry, crs="EPSG:4326")
|
|
|
+
|
|
|
+ self.logger.info(f"✓ 成功转换DataFrame到GeoDataFrame: {len(gdf)} 个点")
|
|
|
+ return gdf
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"DataFrame转GeoDataFrame失败: {str(e)}")
|
|
|
+ raise
|
|
|
+
|
|
|
def create_boundary_mask(self, raster, transform, gdf):
|
|
|
"""
|
|
|
创建边界掩膜,只保留边界内的区域
|
|
@@ -199,8 +246,150 @@ class MappingUtils:
|
|
|
self.logger.error(f"插值失败: {str(e)}")
|
|
|
return raster
|
|
|
|
|
|
+ def geodataframe_to_raster(self, gdf, template_tif, output_tif, field,
|
|
|
+ resolution_factor=16.0, boundary_gdf=None, interpolation_method='nearest', enable_interpolation=True):
|
|
|
+ """
|
|
|
+ 将GeoDataFrame直接转换为栅格数据(内存处理版本)
|
|
|
+
|
|
|
+ @param gdf: 输入的GeoDataFrame
|
|
|
+ @param template_tif: 用作模板的GeoTIFF文件路径
|
|
|
+ @param output_tif: 输出栅格化后的GeoTIFF文件路径
|
|
|
+ @param field: 用于栅格化的属性字段名
|
|
|
+ @param resolution_factor: 分辨率倍数因子
|
|
|
+ @param boundary_gdf: 边界GeoDataFrame,用于创建掩膜
|
|
|
+ @param interpolation_method: 插值方法 ('nearest', 'linear', 'cubic')
|
|
|
+ @param enable_interpolation: 是否启用空间插值,默认True
|
|
|
+ @return: 输出的GeoTIFF文件路径和统计信息
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ self.logger.info(f"开始处理GeoDataFrame到栅格(内存处理)")
|
|
|
+ interpolation_status = "启用" if enable_interpolation else "禁用"
|
|
|
+ self.logger.info(f"分辨率因子: {resolution_factor}, 插值设置: {interpolation_status} (方法: {interpolation_method})")
|
|
|
+
|
|
|
+ # 读取模板栅格
|
|
|
+ with rasterio.open(template_tif) as src:
|
|
|
+ template_meta = src.meta.copy()
|
|
|
+
|
|
|
+ # 根据分辨率因子计算新的尺寸和变换参数
|
|
|
+ if resolution_factor != 1.0:
|
|
|
+ width = int(src.width * resolution_factor)
|
|
|
+ height = int(src.height * resolution_factor)
|
|
|
+
|
|
|
+ transform = rasterio.Affine(
|
|
|
+ src.transform.a / resolution_factor,
|
|
|
+ src.transform.b,
|
|
|
+ src.transform.c,
|
|
|
+ src.transform.d,
|
|
|
+ src.transform.e / resolution_factor,
|
|
|
+ src.transform.f
|
|
|
+ )
|
|
|
+ self.logger.info(f"分辨率调整: {src.width}x{src.height} -> {width}x{height}")
|
|
|
+ else:
|
|
|
+ width = src.width
|
|
|
+ height = src.height
|
|
|
+ transform = src.transform
|
|
|
+ self.logger.info(f"保持原始分辨率: {width}x{height}")
|
|
|
+
|
|
|
+ crs = src.crs
|
|
|
+
|
|
|
+ # 投影矢量数据
|
|
|
+ if gdf.crs != crs:
|
|
|
+ gdf = gdf.to_crs(crs)
|
|
|
+
|
|
|
+ # 栅格化
|
|
|
+ shapes = ((geom, value) for geom, value in zip(gdf.geometry, gdf[field]))
|
|
|
+ raster = rasterize(
|
|
|
+ shapes=shapes,
|
|
|
+ out_shape=(height, width),
|
|
|
+ transform=transform,
|
|
|
+ fill=np.nan,
|
|
|
+ dtype='float32'
|
|
|
+ )
|
|
|
+
|
|
|
+ # 预备掩膜:优先使用行政区边界;若未提供边界,则使用点集凸包限制绘制范围
|
|
|
+ boundary_mask = None
|
|
|
+ if boundary_gdf is not None:
|
|
|
+ self.logger.info("应用边界掩膜: 使用直接提供的GeoDataFrame")
|
|
|
+ # 确保边界GeoDataFrame的CRS与栅格一致
|
|
|
+ if boundary_gdf.crs != crs:
|
|
|
+ boundary_gdf = boundary_gdf.to_crs(crs)
|
|
|
+ boundary_mask = self.create_boundary_mask(raster, transform, boundary_gdf)
|
|
|
+ raster[~boundary_mask] = np.nan
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ # 使用点集凸包作为默认掩膜,避免边界外着色
|
|
|
+ hull = gdf.unary_union.convex_hull
|
|
|
+ hull_gdf = gpd.GeoDataFrame(geometry=[hull], crs=crs)
|
|
|
+ boundary_mask = self.create_boundary_mask(raster, transform, hull_gdf)
|
|
|
+ raster[~boundary_mask] = np.nan
|
|
|
+ self.logger.info("已使用点集凸包限制绘制范围")
|
|
|
+ except Exception as hull_err:
|
|
|
+ self.logger.warning(f"生成点集凸包掩膜失败,可能会出现边界外着色: {str(hull_err)}")
|
|
|
+
|
|
|
+ # 检查栅格数据状态并决定是否插值
|
|
|
+ nan_count = np.isnan(raster).sum()
|
|
|
+ total_pixels = raster.size
|
|
|
+ self.logger.info(f"栅格数据状态: 总像素数 {total_pixels}, NaN像素数 {nan_count} ({nan_count/total_pixels*100:.1f}%)")
|
|
|
+
|
|
|
+ # 使用插值方法填充NaN值(如果启用)
|
|
|
+ if enable_interpolation and nan_count > 0:
|
|
|
+ self.logger.info(f"✓ 启用插值: 使用 {interpolation_method} 方法填充 {nan_count} 个NaN像素...")
|
|
|
+ raster = self.interpolate_nan_values(raster, method=interpolation_method)
|
|
|
+ # 关键修正:插值后再次应用掩膜,确保边界外不被填充
|
|
|
+ if boundary_mask is not None:
|
|
|
+ raster[~boundary_mask] = np.nan
|
|
|
+ final_nan_count = np.isnan(raster).sum()
|
|
|
+ self.logger.info(f"插值完成: 剩余NaN像素数 {final_nan_count}")
|
|
|
+ elif enable_interpolation and nan_count == 0:
|
|
|
+ self.logger.info("✓ 插值已启用,但栅格数据无NaN值,无需插值")
|
|
|
+ elif not enable_interpolation and nan_count > 0:
|
|
|
+ self.logger.info(f"✗ 插值已禁用,保留 {nan_count} 个NaN像素")
|
|
|
+ else:
|
|
|
+ self.logger.info("✓ 栅格数据完整,无需插值")
|
|
|
+
|
|
|
+ # 创建输出目录
|
|
|
+ os.makedirs(os.path.dirname(output_tif), exist_ok=True)
|
|
|
+
|
|
|
+ # 更新元数据
|
|
|
+ template_meta.update({
|
|
|
+ "count": 1,
|
|
|
+ "dtype": 'float32',
|
|
|
+ "nodata": np.nan,
|
|
|
+ "width": width,
|
|
|
+ "height": height,
|
|
|
+ "transform": transform
|
|
|
+ })
|
|
|
+
|
|
|
+ # 保存栅格文件
|
|
|
+ with rasterio.open(output_tif, 'w', **template_meta) as dst:
|
|
|
+ dst.write(raster, 1)
|
|
|
+
|
|
|
+ # 计算统计信息
|
|
|
+ valid_data = raster[~np.isnan(raster)]
|
|
|
+ stats = None
|
|
|
+ if len(valid_data) > 0:
|
|
|
+ stats = {
|
|
|
+ 'min': float(np.min(valid_data)),
|
|
|
+ 'max': float(np.max(valid_data)),
|
|
|
+ 'mean': float(np.mean(valid_data)),
|
|
|
+ 'std': float(np.std(valid_data)),
|
|
|
+ 'valid_pixels': int(len(valid_data)),
|
|
|
+ 'total_pixels': int(raster.size)
|
|
|
+ }
|
|
|
+ self.logger.info(f"统计信息: 有效像素 {stats['valid_pixels']}/{stats['total_pixels']}")
|
|
|
+ self.logger.info(f"数值范围: {stats['min']:.4f} - {stats['max']:.4f}")
|
|
|
+ else:
|
|
|
+ self.logger.warning("没有有效数据")
|
|
|
+
|
|
|
+ self.logger.info(f"✓ 成功保存: {output_tif}")
|
|
|
+ return output_tif, stats
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"GeoDataFrame转栅格失败: {str(e)}")
|
|
|
+ raise
|
|
|
+
|
|
|
def vector_to_raster(self, input_shapefile, template_tif, output_tif, field,
|
|
|
- resolution_factor=16.0, boundary_shp=None, interpolation_method='nearest', enable_interpolation=True):
|
|
|
+ resolution_factor=16.0, boundary_shp=None, boundary_gdf=None, interpolation_method='nearest', enable_interpolation=True):
|
|
|
"""
|
|
|
将点矢量数据转换为栅格数据
|
|
|
|
|
@@ -209,14 +398,16 @@ class MappingUtils:
|
|
|
@param output_tif: 输出栅格化后的GeoTIFF文件路径
|
|
|
@param field: 用于栅格化的属性字段名
|
|
|
@param resolution_factor: 分辨率倍数因子
|
|
|
- @param boundary_shp: 边界Shapefile文件路径,用于创建掩膜
|
|
|
+ @param boundary_shp: 边界Shapefile文件路径,用于创建掩膜(兼容性保留)
|
|
|
+ @param boundary_gdf: 边界GeoDataFrame,优先使用此参数而非boundary_shp
|
|
|
@param interpolation_method: 插值方法 ('nearest', 'linear', 'cubic')
|
|
|
@param enable_interpolation: 是否启用空间插值,默认True
|
|
|
@return: 输出的GeoTIFF文件路径和统计信息
|
|
|
"""
|
|
|
try:
|
|
|
self.logger.info(f"开始处理: {input_shapefile}")
|
|
|
- self.logger.info(f"分辨率因子: {resolution_factor}, 插值方法: {interpolation_method}")
|
|
|
+ interpolation_status = "启用" if enable_interpolation else "禁用"
|
|
|
+ self.logger.info(f"分辨率因子: {resolution_factor}, 插值设置: {interpolation_status} (方法: {interpolation_method})")
|
|
|
|
|
|
# 读取矢量数据
|
|
|
gdf = gpd.read_file(input_shapefile)
|
|
@@ -263,13 +454,20 @@ class MappingUtils:
|
|
|
|
|
|
# 预备掩膜:优先使用行政区边界;若未提供边界,则使用点集凸包限制绘制范围
|
|
|
boundary_mask = None
|
|
|
- if boundary_shp and os.path.exists(boundary_shp):
|
|
|
- self.logger.info(f"应用边界掩膜: {boundary_shp}")
|
|
|
- boundary_gdf = gpd.read_file(boundary_shp)
|
|
|
+ if boundary_gdf is not None:
|
|
|
+ self.logger.info("应用边界掩膜: 使用直接提供的GeoDataFrame")
|
|
|
+ # 确保边界GeoDataFrame的CRS与栅格一致
|
|
|
if boundary_gdf.crs != crs:
|
|
|
boundary_gdf = boundary_gdf.to_crs(crs)
|
|
|
boundary_mask = self.create_boundary_mask(raster, transform, boundary_gdf)
|
|
|
raster[~boundary_mask] = np.nan
|
|
|
+ elif boundary_shp and os.path.exists(boundary_shp):
|
|
|
+ self.logger.info(f"应用边界掩膜: {boundary_shp}")
|
|
|
+ boundary_gdf_from_file = gpd.read_file(boundary_shp)
|
|
|
+ if boundary_gdf_from_file.crs != crs:
|
|
|
+ boundary_gdf_from_file = boundary_gdf_from_file.to_crs(crs)
|
|
|
+ boundary_mask = self.create_boundary_mask(raster, transform, boundary_gdf_from_file)
|
|
|
+ raster[~boundary_mask] = np.nan
|
|
|
else:
|
|
|
try:
|
|
|
# 使用点集凸包作为默认掩膜,避免边界外着色
|
|
@@ -281,15 +479,26 @@ class MappingUtils:
|
|
|
except Exception as hull_err:
|
|
|
self.logger.warning(f"生成点集凸包掩膜失败,可能会出现边界外着色: {str(hull_err)}")
|
|
|
|
|
|
+ # 检查栅格数据状态并决定是否插值
|
|
|
+ nan_count = np.isnan(raster).sum()
|
|
|
+ total_pixels = raster.size
|
|
|
+ self.logger.info(f"栅格数据状态: 总像素数 {total_pixels}, NaN像素数 {nan_count} ({nan_count/total_pixels*100:.1f}%)")
|
|
|
+
|
|
|
# 使用插值方法填充NaN值(如果启用)
|
|
|
- if enable_interpolation and np.isnan(raster).any():
|
|
|
- self.logger.info(f"使用 {interpolation_method} 方法进行插值...")
|
|
|
+ if enable_interpolation and nan_count > 0:
|
|
|
+ self.logger.info(f"✓ 启用插值: 使用 {interpolation_method} 方法填充 {nan_count} 个NaN像素...")
|
|
|
raster = self.interpolate_nan_values(raster, method=interpolation_method)
|
|
|
# 关键修正:插值后再次应用掩膜,确保边界外不被填充
|
|
|
if boundary_mask is not None:
|
|
|
raster[~boundary_mask] = np.nan
|
|
|
- elif not enable_interpolation and np.isnan(raster).any():
|
|
|
- self.logger.info("插值已禁用,保留原始栅格数据(包含NaN值)")
|
|
|
+ final_nan_count = np.isnan(raster).sum()
|
|
|
+ self.logger.info(f"插值完成: 剩余NaN像素数 {final_nan_count}")
|
|
|
+ elif enable_interpolation and nan_count == 0:
|
|
|
+ self.logger.info("✓ 插值已启用,但栅格数据无NaN值,无需插值")
|
|
|
+ elif not enable_interpolation and nan_count > 0:
|
|
|
+ self.logger.info(f"✗ 插值已禁用,保留 {nan_count} 个NaN像素")
|
|
|
+ else:
|
|
|
+ self.logger.info("✓ 栅格数据完整,无需插值")
|
|
|
|
|
|
# 创建输出目录
|
|
|
os.makedirs(os.path.dirname(output_tif), exist_ok=True)
|
|
@@ -336,11 +545,11 @@ class MappingUtils:
|
|
|
colormap='green_yellow_red_purple', title="Prediction Map",
|
|
|
output_size=12, figsize=None, dpi=300,
|
|
|
resolution_factor=1.0, enable_interpolation=True,
|
|
|
- interpolation_method='nearest'):
|
|
|
+ interpolation_method='nearest', boundary_gdf=None):
|
|
|
"""
|
|
|
创建栅格地图
|
|
|
|
|
|
- @param shp_path: 输入的矢量数据路径
|
|
|
+ @param shp_path: 输入的矢量数据路径(兼容性保留)
|
|
|
@param tif_path: 输入的栅格数据路径
|
|
|
@param output_path: 输出图片路径(不包含扩展名)
|
|
|
@param colormap: 色彩方案名称或颜色列表
|
|
@@ -351,14 +560,23 @@ class MappingUtils:
|
|
|
@param resolution_factor: 分辨率因子,>1提高分辨率,<1降低分辨率
|
|
|
@param enable_interpolation: 是否启用空间插值,用于处理NaN值或提高分辨率,默认True
|
|
|
@param interpolation_method: 插值方法 ('nearest', 'linear', 'cubic')
|
|
|
- @return: 输出图片文件路径
|
|
|
+ @param boundary_gdf: 边界GeoDataFrame(可选,优先使用)
|
|
|
+ @return: 输出图片文件路径
|
|
|
"""
|
|
|
try:
|
|
|
self.logger.info(f"开始创建栅格地图: {tif_path}")
|
|
|
self.logger.info(f"分辨率因子: {resolution_factor}, 启用插值: {enable_interpolation}")
|
|
|
|
|
|
- # 读取矢量边界
|
|
|
- gdf = gpd.read_file(shp_path) if shp_path else None
|
|
|
+ # 读取矢量边界:优先使用boundary_gdf,否则从shp_path读取
|
|
|
+ if boundary_gdf is not None:
|
|
|
+ gdf = boundary_gdf
|
|
|
+ self.logger.info("使用直接提供的边界GeoDataFrame")
|
|
|
+ elif shp_path:
|
|
|
+ gdf = gpd.read_file(shp_path)
|
|
|
+ self.logger.info(f"从文件读取边界数据: {shp_path}")
|
|
|
+ else:
|
|
|
+ gdf = None
|
|
|
+ self.logger.info("未提供边界数据,将使用整个栅格范围")
|
|
|
|
|
|
# 读取并裁剪栅格数据
|
|
|
with rasterio.open(tif_path) as src:
|
|
@@ -406,21 +624,38 @@ class MappingUtils:
|
|
|
if np.all(np.isnan(raster)):
|
|
|
raise ValueError("栅格数据中没有有效值")
|
|
|
|
|
|
- # 根据分位数分为6个等级
|
|
|
- bounds = np.nanpercentile(raster, [0, 20, 40, 60, 80, 90, 100])
|
|
|
- norm = BoundaryNorm(bounds, ncolors=len(bounds) - 1)
|
|
|
-
|
|
|
- # 获取色彩方案
|
|
|
- if isinstance(colormap, str):
|
|
|
- if colormap in COLORMAPS:
|
|
|
- color_list = COLORMAPS[colormap]
|
|
|
+ # 检查数据是否为相同值
|
|
|
+ valid_data = raster[~np.isnan(raster)]
|
|
|
+ data_min = np.min(valid_data)
|
|
|
+ data_max = np.max(valid_data)
|
|
|
+
|
|
|
+ if data_min == data_max:
|
|
|
+ # 所有值相同的情况:创建简单的单色映射
|
|
|
+ self.logger.info(f"检测到所有值相同 ({data_min:.6f}),使用单色映射")
|
|
|
+ bounds = [data_min - 0.001, data_min + 0.001] # 创建微小的范围
|
|
|
+ norm = BoundaryNorm(bounds, ncolors=1)
|
|
|
+ # 使用绿色系的第一个颜色作为单色
|
|
|
+ if isinstance(colormap, str) and colormap in COLORMAPS:
|
|
|
+ single_color = COLORMAPS[colormap][0] # 使用色彩方案的第一个颜色
|
|
|
else:
|
|
|
- self.logger.warning(f"未知色彩方案: {colormap},使用默认方案")
|
|
|
- color_list = COLORMAPS['green_yellow_red_purple']
|
|
|
+ single_color = '#89AC46' # 默认绿色
|
|
|
+ cmap = ListedColormap([single_color])
|
|
|
else:
|
|
|
- color_list = colormap
|
|
|
-
|
|
|
- cmap = ListedColormap(color_list)
|
|
|
+ # 正常情况:根据分位数分为6个等级
|
|
|
+ bounds = np.nanpercentile(raster, [0, 20, 40, 60, 80, 90, 100])
|
|
|
+ norm = BoundaryNorm(bounds, ncolors=len(bounds) - 1)
|
|
|
+
|
|
|
+ # 获取色彩方案
|
|
|
+ if isinstance(colormap, str):
|
|
|
+ if colormap in COLORMAPS:
|
|
|
+ color_list = COLORMAPS[colormap]
|
|
|
+ else:
|
|
|
+ self.logger.warning(f"未知色彩方案: {colormap},使用默认方案")
|
|
|
+ color_list = COLORMAPS['green_yellow_red_purple']
|
|
|
+ else:
|
|
|
+ color_list = colormap
|
|
|
+
|
|
|
+ cmap = ListedColormap(color_list)
|
|
|
|
|
|
# 设置图片尺寸
|
|
|
if figsize is not None:
|
|
@@ -461,16 +696,29 @@ class MappingUtils:
|
|
|
ax.tick_params(axis='y', labelrotation=90)
|
|
|
|
|
|
# 添加色带
|
|
|
- tick_labels = [f"{bounds[i]:.1f}" for i in range(len(bounds) - 1)]
|
|
|
- cbar = plt.colorbar(
|
|
|
- plt.cm.ScalarMappable(norm=norm, cmap=cmap),
|
|
|
- ax=ax,
|
|
|
- ticks=[(bounds[i] + bounds[i+1]) / 2 for i in range(len(bounds) - 1)],
|
|
|
- shrink=0.6,
|
|
|
- aspect=15
|
|
|
- )
|
|
|
- cbar.ax.set_yticklabels(tick_labels)
|
|
|
- cbar.set_label("Values")
|
|
|
+ if data_min == data_max:
|
|
|
+ # 单值情况:简化的色带
|
|
|
+ cbar = plt.colorbar(
|
|
|
+ plt.cm.ScalarMappable(norm=norm, cmap=cmap),
|
|
|
+ ax=ax,
|
|
|
+ ticks=[data_min],
|
|
|
+ shrink=0.6,
|
|
|
+ aspect=15
|
|
|
+ )
|
|
|
+ cbar.ax.set_yticklabels([f"{data_min:.6f}"])
|
|
|
+ cbar.set_label("Fixed Value")
|
|
|
+ else:
|
|
|
+ # 正常情况:分级色带
|
|
|
+ tick_labels = [f"{bounds[i]:.1f}" for i in range(len(bounds) - 1)]
|
|
|
+ cbar = plt.colorbar(
|
|
|
+ plt.cm.ScalarMappable(norm=norm, cmap=cmap),
|
|
|
+ ax=ax,
|
|
|
+ ticks=[(bounds[i] + bounds[i+1]) / 2 for i in range(len(bounds) - 1)],
|
|
|
+ shrink=0.6,
|
|
|
+ aspect=15
|
|
|
+ )
|
|
|
+ cbar.ax.set_yticklabels(tick_labels)
|
|
|
+ cbar.set_label("Values")
|
|
|
|
|
|
plt.tight_layout()
|
|
|
|
|
@@ -585,13 +833,26 @@ class MappingUtils:
|
|
|
if len(band_flat) == 0:
|
|
|
raise ValueError("栅格数据中没有有效值")
|
|
|
|
|
|
+ # 检查是否所有值相同
|
|
|
+ data_min = np.min(band_flat)
|
|
|
+ data_max = np.max(band_flat)
|
|
|
+
|
|
|
# 创建图形
|
|
|
plt.figure(figsize=figsize)
|
|
|
|
|
|
- # 绘制直方图和密度曲线
|
|
|
- sns.histplot(band_flat, bins=bins, color='steelblue', alpha=0.7,
|
|
|
- edgecolor='black', stat='density')
|
|
|
- sns.kdeplot(band_flat, color='red', linewidth=2)
|
|
|
+ if data_min == data_max:
|
|
|
+ # 所有值相同:创建特殊的单值直方图
|
|
|
+ self.logger.info(f"检测到所有值相同 ({data_min:.6f}),创建单值直方图")
|
|
|
+ plt.bar([data_min], [len(band_flat)], width=0.1*abs(data_min) if data_min != 0 else 0.1,
|
|
|
+ color='steelblue', alpha=0.7, edgecolor='black')
|
|
|
+ plt.axvline(x=data_min, color='red', linewidth=2, linestyle='--',
|
|
|
+ label=f'Fixed Value: {data_min:.6f}')
|
|
|
+ plt.legend()
|
|
|
+ else:
|
|
|
+ # 正常情况:绘制直方图和密度曲线
|
|
|
+ sns.histplot(band_flat, bins=bins, color='steelblue', alpha=0.7,
|
|
|
+ edgecolor='black', stat='density')
|
|
|
+ sns.kdeplot(band_flat, color='red', linewidth=2)
|
|
|
|
|
|
# 设置标签和标题
|
|
|
plt.xlabel(xlabel, fontsize=14)
|
|
@@ -627,17 +888,65 @@ def get_available_colormaps():
|
|
|
return COLORMAPS.copy()
|
|
|
|
|
|
|
|
|
+def dataframe_to_raster_workflow(df, template_tif, output_dir,
|
|
|
+ boundary_gdf=None, resolution_factor=16.0,
|
|
|
+ interpolation_method='nearest', field_name='Prediction',
|
|
|
+ lon_col=0, lat_col=1, value_col=2, enable_interpolation=False):
|
|
|
+ """
|
|
|
+ DataFrame到栅格转换工作流(内存处理优化版本)
|
|
|
+
|
|
|
+ @param df: pandas DataFrame
|
|
|
+ @param template_tif: 模板GeoTIFF文件路径
|
|
|
+ @param output_dir: 输出目录
|
|
|
+ @param boundary_gdf: 边界GeoDataFrame(可选)
|
|
|
+ @param resolution_factor: 分辨率因子
|
|
|
+ @param interpolation_method: 插值方法
|
|
|
+ @param field_name: 字段名称
|
|
|
+ @param lon_col: 经度列
|
|
|
+ @param lat_col: 纬度列
|
|
|
+ @param value_col: 数值列
|
|
|
+ @param enable_interpolation: 是否启用空间插值,默认False
|
|
|
+ @return: 输出文件路径字典
|
|
|
+ """
|
|
|
+ mapper = MappingUtils()
|
|
|
+
|
|
|
+ # 确保输出目录存在
|
|
|
+ os.makedirs(output_dir, exist_ok=True)
|
|
|
+
|
|
|
+ # 生成文件名(基于时间戳,避免冲突)
|
|
|
+ import time
|
|
|
+ timestamp = str(int(time.time()))
|
|
|
+ raster_path = os.path.join(output_dir, f"memory_raster_{timestamp}.tif")
|
|
|
+
|
|
|
+ # 1. DataFrame直接转GeoDataFrame(内存处理)
|
|
|
+ gdf = mapper.dataframe_to_geodataframe(df, lon_col, lat_col, value_col, field_name)
|
|
|
+
|
|
|
+ # 2. GeoDataFrame直接转栅格(内存处理)
|
|
|
+ raster_path, stats = mapper.geodataframe_to_raster(
|
|
|
+ gdf, template_tif, raster_path, field_name,
|
|
|
+ resolution_factor, boundary_gdf, interpolation_method, enable_interpolation
|
|
|
+ )
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'shapefile': None, # 内存处理不生成shapefile
|
|
|
+ 'raster': raster_path,
|
|
|
+ 'statistics': stats,
|
|
|
+ 'geodataframe': gdf # 返回GeoDataFrame供调试使用
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
def csv_to_raster_workflow(csv_file, template_tif, output_dir,
|
|
|
- boundary_shp=None, resolution_factor=16.0,
|
|
|
+ boundary_shp=None, boundary_gdf=None, resolution_factor=16.0,
|
|
|
interpolation_method='nearest', field_name='Prediction',
|
|
|
lon_col=0, lat_col=1, value_col=2, enable_interpolation=False):
|
|
|
"""
|
|
|
- 完整的CSV到栅格转换工作流
|
|
|
+ 完整的CSV到栅格转换工作流(原版本,保持兼容性)
|
|
|
|
|
|
@param csv_file: CSV文件路径
|
|
|
@param template_tif: 模板GeoTIFF文件路径
|
|
|
@param output_dir: 输出目录
|
|
|
- @param boundary_shp: 边界Shapefile文件路径(可选)
|
|
|
+ @param boundary_shp: 边界Shapefile文件路径(可选,兼容性保留)
|
|
|
+ @param boundary_gdf: 边界GeoDataFrame(可选,优先使用)
|
|
|
@param resolution_factor: 分辨率因子
|
|
|
@param interpolation_method: 插值方法
|
|
|
@param field_name: 字段名称
|
|
@@ -663,7 +972,7 @@ def csv_to_raster_workflow(csv_file, template_tif, output_dir,
|
|
|
# 2. Shapefile转栅格
|
|
|
raster_path, stats = mapper.vector_to_raster(
|
|
|
shapefile_path, template_tif, raster_path, field_name,
|
|
|
- resolution_factor, boundary_shp, interpolation_method, enable_interpolation
|
|
|
+ resolution_factor, boundary_shp, boundary_gdf, interpolation_method, enable_interpolation
|
|
|
)
|
|
|
|
|
|
return {
|