Figure_raster_mapping.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. """
  2. Author: Wanxue Zhu
  3. Time: 2025 - April - 18
  4. Description: 这个代码用于绘制栅格地图,第一个函数是绘制地图的,第二个是绘制栅格数据的直方图
  5. """
  6. import json
  7. import os
  8. import logging
  9. import shutil
  10. import tempfile
  11. from datetime import datetime
  12. from pathlib import Path
  13. import geopandas as gpd
  14. import matplotlib.pyplot as plt
  15. from matplotlib.colors import ListedColormap, BoundaryNorm
  16. from rasterio.mask import mask
  17. from rasterio.plot import show
  18. import numpy as np
  19. import rasterio
  20. import seaborn as sns
  21. # 设定一些常用的colormap
  22. colormap1 = ['#FFFECE', '#FFF085', '#FEBA17','#BE3D2A','#74512D', '#4E1F00'] #黄-橙-棕
  23. colormap2 = ['#F6F8D5', '#98D2C0', '#4F959D', '#205781', '#143D60','#2A3335'] #蓝色系
  24. colormap3 = ['#FFEFC8', '#F8ED8C', '#D3E671', '#89AC46', '#5F8B4C', '#355F2E']#淡黄-草绿
  25. colormap4 = ['#F0F1C5', '#BBD8A3', '#6F826A', '#BF9264', '#735557', '#604652']#绿色-棕色
  26. colormap5 = ['#FCFAEE', '#FBF3B9', '#FFDCCC', '#FDB7EA', '#B7B1F2', '#8D77AB']#黄-粉-紫
  27. colormap6 = ['#15B392', '#73EC8B', '#FFEB55', '#EE66A6', '#D91656', '#640D5F']#绿-黄-红-紫
  28. # 配置日志
  29. logger = logging.getLogger(__name__)
  30. logger.setLevel(logging.INFO)
  31. handler = logging.StreamHandler()
  32. formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  33. handler.setFormatter(formatter)
  34. logger.addHandler(handler)
  35. # 设置全局字体为 Arial,并设置中文字体(SimHei或Microsoft YaHei)
  36. plt.rcParams['font.family'] = 'Arial'
  37. plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
  38. plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 添加多个中文字体
  39. def mapping_raster(shp_path, tif_path, color_map_name, title_name, output_path, output_size):
  40. """
  41. 生成栅格地图可视化
  42. 参数:
  43. - shp_path: 输入的矢量数据的路径
  44. - tif_path: 输入的栅格数据的路径
  45. - color_map_name: 使用的色彩方案
  46. - title_name: 输出数据的图的名称
  47. - output_path: 输出保存的图片的完整路径(包括扩展名)
  48. - output_size: 输出图片的尺寸(英寸)
  49. """
  50. try:
  51. logger.info(f"开始生成栅格地图: shp={shp_path}, tif={tif_path}, output={output_path}")
  52. # 确保输出目录存在
  53. output_dir = os.path.dirname(output_path)
  54. if output_dir:
  55. os.makedirs(output_dir, exist_ok=True)
  56. # 读取矢量边界
  57. logger.info("读取矢量边界...")
  58. gdf = gpd.read_file(shp_path)
  59. logger.info(f"矢量数据包含 {len(gdf)} 个要素")
  60. if gdf.empty:
  61. logger.warning("矢量数据为空,尝试重新读取")
  62. gdf = gpd.read_file(shp_path) # 再次尝试
  63. if gdf.empty:
  64. raise ValueError("矢量数据为空")
  65. # 读取并裁剪栅格数据
  66. logger.info("处理栅格数据...")
  67. with rasterio.open(tif_path) as src:
  68. logger.info(f"栅格尺寸: {src.width}x{src.height}, 波段数: {src.count}")
  69. # 确保几何体格式正确
  70. if gdf.crs is not None and gdf.crs != src.crs:
  71. logger.info(f"转换矢量CRS: {gdf.crs} -> {src.crs}")
  72. gdf = gdf.to_crs(src.crs)
  73. # 确保转换后的几何体有效
  74. if gdf.is_empty.any():
  75. logger.warning("转换后几何体为空,使用原始几何体")
  76. gdf = gpd.read_file(shp_path) # 回退到原始数据
  77. geoms = [json.loads(gdf.to_json())["features"][0]["geometry"]]
  78. out_image, out_transform = mask(src, geoms, crop=True)
  79. out_meta = src.meta.copy()
  80. # 提取数据并处理无效值
  81. raster = out_image[0].astype('float16')
  82. nodata = out_meta.get("nodata", None)
  83. logger.info(f"栅格数据范围: {raster.min()} - {raster.max()}, nodata={nodata}")
  84. if nodata is not None:
  85. raster[raster == nodata] = np.nan
  86. # 处理无穷大值
  87. raster[np.isinf(raster)] = np.nan
  88. # 验证数据有效性
  89. valid_mask = ~np.isnan(raster)
  90. valid_count = np.sum(valid_mask)
  91. logger.info(f"处理无效值后有效点数: {valid_count}")
  92. # 检查是否有有效数据
  93. if valid_count == 0:
  94. logger.warning("裁剪后的栅格不包含有效数据,使用默认值填充")
  95. raster = np.zeros_like(raster) # 使用0填充
  96. # 确保数据范围有效
  97. min_val = np.nanmin(raster)
  98. max_val = np.nanmax(raster)
  99. # 如果数据范围太小,设置为合理范围
  100. if min_val == max_val:
  101. min_val = min_val - 0.1 if min_val > 0 else -0.1
  102. max_val = max_val + 0.1
  103. logger.info(f"有效数据范围: {min_val} - {max_val}")
  104. # 根据分位数分为6个等级
  105. logger.info("计算数据分位数...")
  106. try:
  107. bounds = np.nanpercentile(raster, [0, 20, 40, 60, 80, 90, 100])
  108. except Exception as e:
  109. logger.warning(f"分位数计算失败: {str(e)},使用线性范围")
  110. bounds = np.linspace(min_val, max_val, 7)
  111. # 确保分位数有效
  112. if np.any(np.isnan(bounds)) or np.any(np.isinf(bounds)):
  113. logger.warning("分位数包含无效值,使用线性范围")
  114. bounds = np.linspace(min_val, max_val, 7)
  115. logger.info(f"分位数: {bounds}")
  116. norm = BoundaryNorm(bounds, ncolors=len(bounds) - 1)
  117. # 自定义颜色设定
  118. cmap = ListedColormap(color_map_name)
  119. # 绘图
  120. logger.info("创建图表...")
  121. fig, ax = plt.subplots(figsize=(output_size, output_size))
  122. logger.info(f"创建图表完成: 尺寸={output_size}英寸")
  123. # 显示栅格数据
  124. logger.info("显示栅格数据...")
  125. show(raster, transform=out_transform, ax=ax, cmap=cmap, norm=norm)
  126. logger.info("栅格数据显示完成")
  127. # 添加矢量边界
  128. gdf.boundary.plot(ax=ax, color='black', linewidth=1)
  129. logger.info("矢量边界添加完成")
  130. # 设置标题和标签
  131. ax.set_title(title_name, fontsize=20)
  132. ax.set_xlabel("Longitude", fontsize=18)
  133. ax.set_ylabel("Latitude", fontsize=18)
  134. # 添加经纬网
  135. ax.grid(True, linestyle='--', color='gray', alpha=0.5)
  136. ax.tick_params(axis='y', labelrotation=90)
  137. logger.info("坐标轴设置完成")
  138. # 创建图例
  139. tick_labels = [f"{bounds[i]:.1f}" for i in range(len(bounds) - 1)]
  140. cbar = plt.colorbar(
  141. plt.cm.ScalarMappable(norm=norm, cmap=cmap),
  142. ax=ax,
  143. ticks=[(bounds[i] + bounds[i + 1]) / 2 for i in range(len(bounds) - 1)],
  144. shrink=0.6,
  145. aspect=15
  146. )
  147. cbar.ax.set_yticklabels(tick_labels)
  148. cbar.set_label("Values")
  149. logger.info("图例创建完成")
  150. # 保存图像
  151. logger.info(f"保存图像到: {output_path}")
  152. plt.tight_layout()
  153. plt.savefig(output_path, dpi=300)
  154. plt.close(fig)
  155. logger.info("图像保存完成")
  156. # 验证文件是否创建成功
  157. if not os.path.exists(output_path):
  158. raise RuntimeError(f"图像文件创建失败: {output_path}")
  159. logger.info("栅格地图生成成功")
  160. return output_path
  161. except rasterio.RasterioIOError as e:
  162. logger.error(f"栅格文件读取错误: {str(e)}")
  163. raise
  164. except ValueError as e:
  165. logger.error(f"数据值错误: {str(e)}")
  166. raise
  167. except Exception as e:
  168. logger.error(f"栅格地图生成失败: {str(e)}", exc_info=True)
  169. raise RuntimeError(f"地图可视化错误: {str(e)}")
  170. def plot_tif_histogram(file_path, figsize=(10, 6), xlabel='像元值', ylabel='频率密度',
  171. title='GeoTIFF 波段值分布图', save_path=None):
  172. """
  173. 生成TIFF文件的直方图
  174. 参数:
  175. - file_path (str): GeoTIFF 文件路径
  176. - figsize (tuple): 图像尺寸,如 (10, 6)
  177. - xlabel (str): 横坐标标签
  178. - ylabel (str): 纵坐标标签
  179. - title (str): 图标题
  180. - save_path (str): 可选,保存图片路径(含文件名和扩展名)
  181. """
  182. try:
  183. logger.info(f"开始生成直方图: {file_path}")
  184. sns.set(style='ticks')
  185. with rasterio.open(file_path) as src:
  186. band = src.read(1)
  187. nodata = src.nodata
  188. if nodata is not None:
  189. band = np.where(band == nodata, np.nan, band)
  190. band_flat = band.flatten()
  191. band_flat = band_flat[~np.isnan(band_flat)]
  192. plt.figure(figsize=figsize)
  193. sns.histplot(band_flat, bins=100, color='steelblue', alpha=0.7, edgecolor='black', stat='density')
  194. sns.kdeplot(band_flat, color='red', linewidth=2)
  195. plt.xlabel(xlabel, fontsize=14)
  196. plt.ylabel(ylabel, fontsize=14)
  197. plt.title(title, fontsize=16)
  198. plt.grid(True, linestyle='--', alpha=0.5)
  199. plt.tight_layout()
  200. if save_path:
  201. # 确保保存目录存在
  202. os.makedirs(os.path.dirname(save_path), exist_ok=True)
  203. plt.savefig(save_path, dpi=300, format='jpg', bbox_inches='tight')
  204. logger.info(f"保存图像到: {save_path}")
  205. else:
  206. save_path = os.path.join(tempfile.gettempdir(), "tif_histogram.jpg")
  207. plt.savefig(save_path, dpi=300, format='jpg', bbox_inches='tight')
  208. logger.info(f"保存临时图像到: {save_path}")
  209. plt.close()
  210. return save_path
  211. except Exception as e:
  212. logger.error(f"直方图生成失败: {str(e)}", exc_info=True)
  213. raise RuntimeError(f"直方图生成错误: {str(e)}")
  214. def main():
  215. """主函数用于测试栅格地图和直方图绘制功能"""
  216. try:
  217. # 创建静态目录用于保存结果
  218. static_dir = r"D:\17417\Documents\backend\Water\Figures" # 静态目录名称
  219. os.makedirs(static_dir, exist_ok=True) # 确保目录存在
  220. logger.info(f"使用静态目录: {os.path.abspath(static_dir)}")
  221. # ================== 栅格地图绘制测试 ==================
  222. logger.info("开始测试栅格地图绘制功能...")
  223. test_shp = r"D:\17417\Documents\backend\Water\Raster\lechang.shp" # 替换为实际路径
  224. test_tif = r"D:\destkop\DQCJ12321311.tif" # 替换为实际路径
  225. test_colormap = colormap6
  226. test_title = "Atmospheric input flux"
  227. # 在静态目录中创建唯一文件名(添加时间戳避免覆盖)
  228. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  229. output_file = os.path.join(static_dir, f"raster_map_{timestamp}.jpg")
  230. # 调用栅格地图绘制函数
  231. output_file = mapping_raster(
  232. shp_path=test_shp,
  233. tif_path=test_tif,
  234. color_map_name=test_colormap,
  235. title_name=test_title,
  236. output_path=output_file, # 直接使用完整路径
  237. output_size=8
  238. )
  239. if os.path.exists(output_file):
  240. logger.info(f"✅ 栅格地图绘制测试完成: {os.path.abspath(output_file)}")
  241. else:
  242. logger.error(f"❌ 栅格地图文件未创建: {output_file}")
  243. # ================== 直方图绘制测试 ==================
  244. logger.info("开始测试直方图绘制功能...")
  245. histogram_file = test_tif
  246. histogram_output = os.path.join(static_dir, f"histogram_{timestamp}.jpg")
  247. # 调用直方图绘制函数
  248. hist_file = plot_tif_histogram(
  249. file_path=histogram_file,
  250. figsize=(8, 8),
  251. xlabel='Cd content',
  252. ylabel='Frequency',
  253. title='Atmospheric input flux',
  254. save_path=histogram_output
  255. )
  256. if os.path.exists(hist_file):
  257. logger.info(f"✅ 直方图绘制测试完成: {os.path.abspath(hist_file)}")
  258. else:
  259. logger.error(f"❌ 直方图文件未创建: {hist_file}")
  260. logger.info("🎉 所有测试完成! 结果图片已保存到静态目录")
  261. except FileNotFoundError as e:
  262. logger.error(f"❌ 文件未找到错误: {e}")
  263. except rasterio.RasterioIOError:
  264. logger.error("❌ 栅格文件读取失败,请检查文件格式和路径")
  265. except Exception as e:
  266. logger.error(f"❌ 测试过程中发生未知错误: {str(e)}", exc_info=True)
  267. if __name__ == "__main__":
  268. # 安全执行区域
  269. logger.info("=" * 50)
  270. logger.info("栅格地图和直方图绘制测试程序")
  271. logger.info("=" * 50)
  272. # 执行测试
  273. main()