|
@@ -124,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):
|
|
|
"""
|
|
|
创建边界掩膜,只保留边界内的区域
|
|
@@ -202,6 +246,148 @@ 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, boundary_gdf=None, interpolation_method='nearest', enable_interpolation=True):
|
|
|
"""
|
|
@@ -702,12 +888,59 @@ 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, 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文件路径
|