makeMetadate.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. # -*- coding: utf-8 -*-
  2. __author__ = 'wanger'
  3. __description__ = '批量生成栅格数据元数据表格 EXCEL'
  4. __date__ = '2024-11-25'
  5. __copyright__ = '(C) 2024 by siwei'
  6. __revision__ = '1.0'
  7. import os
  8. from osgeo import gdal, osr
  9. import openpyxl
  10. from qgis.core import (
  11. QgsRasterLayer,
  12. QgsVectorLayer,
  13. QgsSpatialIndex,
  14. QgsFeatureRequest,
  15. QgsGeometry
  16. )
  17. from qgis.PyQt.QtCore import NULL
  18. from qgis.PyQt.QtCore import QCoreApplication
  19. from qgis.core import (QgsProcessing,
  20. QgsFeatureSink,
  21. QgsExpression,
  22. QgsFeatureRequest,
  23. QgsVectorLayer,
  24. QgsProcessingException,
  25. QgsProcessingAlgorithm,
  26. QgsProcessingParameterFeatureSource,
  27. QgsProcessingParameterFolderDestination,
  28. QgsProcessingParameterString,
  29. QgsProcessingParameterFile,
  30. QgsProcessingParameterFeatureSink)
  31. from qgis import processing
  32. class MakeMetadataProcessingAlgorithm(QgsProcessingAlgorithm):
  33. rasterfolder = 'rasterfolder'
  34. template = 'template'
  35. outputfolder = 'outputfolder'
  36. # 图幅xy标识位取值 5:8,8:11
  37. splittingnum = 'splittingnum'
  38. splittingnumdefault = '5:8,8:11'
  39. def tr(self, string):
  40. return QCoreApplication.translate('Processing', string)
  41. def createInstance(self):
  42. return MakeMetadataProcessingAlgorithm()
  43. def name(self):
  44. return 'makeMetadata'
  45. def displayName(self):
  46. return self.tr('生成栅格元数据')
  47. def group(self):
  48. return self.tr('栅格数据')
  49. def groupId(self):
  50. return 'metadata'
  51. def shortHelpString(self):
  52. return self.tr("将选择文件夹内的所有栅格数据根据元数据模板动态生成元数据表。")
  53. def initAlgorithm(self, config=None):
  54. self.addParameter(QgsProcessingParameterFolderDestination(self.rasterfolder, '分幅数据文件夹'))
  55. self.addParameter(QgsProcessingParameterFile(self.template, '模板文件', extension='xlsx'))
  56. self.addParameter(QgsProcessingParameterFolderDestination(self.outputfolder, '元数据输出文件夹'))
  57. self.addParameter(
  58. QgsProcessingParameterString(self.splittingnum, self.tr('图幅xy标识位'), self.splittingnumdefault))
  59. # 通过文件路径来获取文件名和文件后缀
  60. def getImageNumberAndFormat(self, filepath):
  61. # 获取文件名(包括后缀)
  62. file_name = os.path.basename(filepath)
  63. # 获取文件名(不包括后缀)
  64. file_name_no_ext = os.path.splitext(file_name)[0]
  65. # 获取文件后缀
  66. file_ext = os.path.splitext(file_name)[1][1:]
  67. return file_name_no_ext, file_ext
  68. # 通过文件路径获取文件大小
  69. def getFileSize(self, filepath):
  70. try:
  71. file_size = os.path.getsize(filepath)
  72. size_mb = file_size / (1024 * 1024)
  73. return f"{size_mb:.2f}"
  74. except FileNotFoundError:
  75. print("文件未找到,请检查路径是否正确!")
  76. return 0
  77. # 获取左上角点
  78. def getImageLeftTop(self, dataset):
  79. # 获取地理变换参数
  80. geotransform = dataset.GetGeoTransform()
  81. if not geotransform:
  82. raise ValueError("无法获取地理变换参数!")
  83. # 左上角像素的坐标(投影坐标系)
  84. x_min = geotransform[0]
  85. y_max = geotransform[3]
  86. # 获取栅格的空间参考(投影坐标系)
  87. spatial_ref = osr.SpatialReference()
  88. spatial_ref.ImportFromWkt(dataset.GetProjection())
  89. # 转换为地理坐标系(经纬度)
  90. if spatial_ref.IsProjected():
  91. # spatial_ref_geo = spatial_ref.CloneGeogCS()
  92. wgs84_crs = osr.SpatialReference()
  93. wgs84_crs.ImportFromEPSG(4326)
  94. transform = osr.CoordinateTransformation(spatial_ref, wgs84_crs)
  95. lon, lat, _ = transform.TransformPoint(x_min, y_max)
  96. else:
  97. lon, lat = x_min, y_max # 如果已经是地理坐标系,直接使用
  98. return {
  99. "lon": lat,
  100. "lat": lon,
  101. "y": int(x_min),
  102. "x": int(y_max)
  103. }
  104. # 通过栅格数据与矢量数据叠加分析 获取相交面积最大的一块 返回要素
  105. def get_largest_overlapping_feature(self, raster_path, shp_path):
  106. """
  107. 查询与栅格相交的矢量要素中面积最大的一块,并返回指定字段的值。
  108. :param raster_path: 栅格文件路径
  109. :param shp_path: 矢量文件路径
  110. :return: 面积最大的要素
  111. """
  112. # 加载栅格图层
  113. raster_layer = QgsRasterLayer(raster_path, "Raster Layer")
  114. if not raster_layer.isValid():
  115. raise Exception(f"栅格文件无效: {raster_path}")
  116. # 加载矢量图层
  117. vector_layer = QgsVectorLayer(shp_path, "Vector Layer", "ogr")
  118. if not vector_layer.isValid():
  119. raise Exception(f"SHP文件无效: {shp_path}")
  120. # 获取栅格的范围
  121. raster_extent = raster_layer.extent()
  122. # 创建空间索引以加速查询
  123. spatial_index = QgsSpatialIndex(vector_layer.getFeatures())
  124. # 查询与栅格范围相交的要素ID
  125. intersecting_ids = spatial_index.intersects(raster_extent)
  126. # 遍历这些要素并计算相交区域的面积
  127. max_area = 0
  128. largest_feature = None
  129. for fid in intersecting_ids:
  130. feature = vector_layer.getFeature(fid)
  131. intersection = feature.geometry().intersection(QgsGeometry.fromRect(raster_extent))
  132. # 检查是否有有效相交区域
  133. if intersection.isEmpty():
  134. continue
  135. # 计算相交区域的面积
  136. area = intersection.area()
  137. # 找到面积最大的要素
  138. if area > max_area:
  139. max_area = area
  140. largest_feature = feature
  141. if largest_feature:
  142. return largest_feature
  143. else:
  144. return None
  145. # 替换字符串 type (first,last)
  146. def replacestr(self, string, oldvalue, newvalue, type):
  147. oldvalue = str(oldvalue)
  148. newvalue = str(newvalue)
  149. if type == "first":
  150. return string.replace(oldvalue, newvalue, 1)
  151. elif type == "last":
  152. # 查找最后一次出现的 oldvalue 的位置
  153. index = string.rfind(oldvalue)
  154. if index != -1:
  155. # 用切片将 last occurrence 替换
  156. return string[:index] + newvalue + string[index + len(oldvalue):]
  157. # 获取接边情况
  158. def getSplitting(self, image_number, image_format, imagex, imagey, length, folder_path, selectfeature):
  159. east = f"{self.replacestr(string=image_number, oldvalue=str(imagey).zfill(length), newvalue=str(imagey + 1).zfill(length), type='last')}.{image_format}"
  160. west = f"{self.replacestr(string=image_number, oldvalue=str(imagey).zfill(length), newvalue=str(imagey - 1).zfill(length), type='last')}.{image_format}"
  161. north = f"{self.replacestr(string=image_number, oldvalue=str(imagex).zfill(length), newvalue=str(imagex - 1).zfill(length), type='first')}.{image_format}"
  162. south = f"{self.replacestr(string=image_number, oldvalue=str(imagex).zfill(length), newvalue=str(imagex + 1).zfill(length), type='first')}.{image_format}"
  163. print(selectfeature["east"])
  164. print(selectfeature["west"])
  165. print(selectfeature["north"])
  166. print(selectfeature["south"])
  167. return {
  168. "east": selectfeature["east"] if selectfeature["east"] != NULL else self.hasFile(east, folder_path),
  169. "west": selectfeature["west"] if selectfeature["west"] != NULL else self.hasFile(west, folder_path),
  170. "north": selectfeature["north"] if selectfeature["north"] != NULL else self.hasFile(north, folder_path),
  171. "south": selectfeature["south"] if selectfeature["south"] != NULL else self.hasFile(south, folder_path),
  172. }
  173. # 判断有没有文件
  174. def hasFile(self, filepath, folder_path):
  175. if os.path.exists(f"{folder_path}\\{filepath}"):
  176. return "已接"
  177. else:
  178. return "未接"
  179. # 处理数值
  180. def processingNumericalValues(self, value):
  181. print(value)
  182. # return value
  183. if isinstance(value, str):
  184. try:
  185. value = float(value)
  186. except ValueError:
  187. print(f"无法将 '{value}' 转换为浮动数")
  188. return None
  189. if value.is_integer():
  190. return int(value)
  191. else:
  192. return round(value, 2)
  193. # 经纬度转度分秒
  194. def decimal_to_dms(self, deg):
  195. # 计算度(Degrees)
  196. degrees = int(deg)
  197. # 计算分(Minutes),并截取小数部分
  198. minutes = int((deg - degrees) * 60)
  199. # 计算秒(Seconds),保留两位小数
  200. seconds = round((deg - degrees - minutes / 60) * 3600, 2)
  201. return f"{degrees}{minutes}{seconds}"
  202. # 运行
  203. def processAlgorithm(self, parameters, context, feedback):
  204. # 获取模板文件夹
  205. template_file = self.parameterAsString(parameters, self.template, context)
  206. # 栅格数据文件夹
  207. raster_folder = self.parameterAsString(parameters, self.rasterfolder, context)
  208. # 元数据生成目录
  209. output_folder = self.parameterAsString(parameters, self.outputfolder, context)
  210. # 通过图号取接边数据标识位
  211. splittingnum = self.parameterAsString(parameters, self.splittingnum, context)
  212. print(template_file, raster_folder, output_folder)
  213. # TODO 栅格数据文件夹
  214. folder_path = raster_folder
  215. # TODO 元数据输出文件夹
  216. output_path = output_folder
  217. # TODO 元数据模板路径
  218. templatepath = template_file
  219. # 获取元数据模板文件所在的目录
  220. templatedirectory = os.path.dirname(os.path.abspath(templatepath))
  221. # TODO 空间分析图层位置 默认在模板文件夹内有一个overlap.shp 点线面要素无所谓
  222. overlapVectorPath = f"{templatedirectory}\\overlap.shp"
  223. # TODO 网格数据查询 默认在模板文件夹内有一个fishnet.shp 点线面要素无所谓
  224. fishVectorPath = f"{templatedirectory}\\fishnet.shp"
  225. fishlayer = QgsVectorLayer(fishVectorPath, "Fish Layer", "ogr")
  226. fish_field_name = "imagenum"
  227. # 绑定需要从shp数据提取的信息
  228. # data_gain_date 影像/数据获取时间
  229. # data_resolution 影像分辨率/点云密度
  230. overlapParams = {
  231. "data_resolution": "resolution",
  232. "data_gain_date": "gaindate",
  233. "source_type": "sourcetype"
  234. }
  235. # 记录总计生成了多少个元数据表
  236. totalNumber = 0
  237. # 遍历文件夹中的所有文件
  238. for filename in os.listdir(folder_path):
  239. filepath = os.path.join(folder_path, filename)
  240. # 检查文件扩展名
  241. if filename.endswith('.img') or filename.endswith('.tif'):
  242. dataset = gdal.Open(filepath)
  243. # TODO 读取图号
  244. image_number, image_format = self.getImageNumberAndFormat(filepath)
  245. file_size = self.getFileSize(filepath)
  246. # 通过图号获取网格要素
  247. sql_query = f"strpos('{image_number}', \"{fish_field_name}\") > 0"
  248. expression = QgsExpression(sql_query)
  249. request = QgsFeatureRequest(expression)
  250. # 获取查询结果
  251. matching_features = [f for f in fishlayer.getFeatures(request)]
  252. selectfeature = matching_features[0]
  253. if dataset:
  254. # TODO 获取栅格数据的行数、列数
  255. grid_row = dataset.RasterYSize
  256. grid_col = dataset.RasterXSize
  257. # TODO波段数
  258. band_size = dataset.RasterCount
  259. # TODO 无效值 list 用逗号隔开
  260. nodata_list = []
  261. # TODO 计算高程记录的小数点位数
  262. decimal_number = None
  263. for band_index in range(1, dataset.RasterCount + 1):
  264. band = dataset.GetRasterBand(band_index)
  265. nodata_list.append(band.GetNoDataValue())
  266. stats = band.GetStatistics(True, True) # 获取统计信息
  267. min_value, max_value = stats[0], stats[1]
  268. if min_value is not None and max_value is not None:
  269. # 获取高程小数位数的最大可能值
  270. decimal_number = max(len(str(min_value).split(".")[-1]),
  271. len(str(max_value).split(".")[-1])) if '.' in str(
  272. min_value) else 0
  273. # TODO 获取栅格数据左上角标
  274. cornerpoint = self.getImageLeftTop(dataset)
  275. # 获取地理信息
  276. geo_transform = dataset.GetGeoTransform()
  277. # TODO 格网单元尺寸
  278. x_resolution = abs(geo_transform[1]) # 像素宽度
  279. y_resolution = abs(geo_transform[5]) # 像素高度
  280. grid_size = f"{self.processingNumericalValues(x_resolution)}*{self.processingNumericalValues(y_resolution)}"
  281. # grid_size = f"{x_resolution}*{y_resolution}"
  282. # 获取投影信息
  283. projection = dataset.GetProjection()
  284. # 获取栅格数据的投影信息
  285. # 创建 SpatialReference 对象并加载投影信息
  286. spatial_ref = osr.SpatialReference()
  287. spatial_ref.ImportFromWkt(projection)
  288. # TODO 投影参数集合
  289. central_meridian = None
  290. ellipsoid_radius = None
  291. ellipsoid_flat = None
  292. geodetic_datum = None
  293. projection_name = None
  294. divided_into_zones = "无分带"
  295. with_number = "无代号"
  296. plane_coordinate_unit = "米"
  297. altitude_name = "正常高"
  298. altitude_benchmark = "1985国家高程基准"
  299. if spatial_ref.IsProjected():
  300. # TODO 中央子午线
  301. central_meridian = self.processingNumericalValues(
  302. spatial_ref.GetProjParm(osr.SRS_PP_CENTRAL_MERIDIAN))
  303. # TODO 椭球长半径
  304. ellipsoid_radius = self.processingNumericalValues(spatial_ref.GetSemiMajor()) # 长半轴
  305. # TODO 椭球扁率
  306. ellipsoid_flat = f"1/{spatial_ref.GetInvFlattening():.3f}" # 反扁率(1/扁率)
  307. # TODO 所采用大地基准
  308. geodetic_datum = spatial_ref.GetAttrValue("DATUM")
  309. # TODO 地图投影名称
  310. projection_name = spatial_ref.GetAttrValue("PROJECTION")
  311. # TODO 分带方式(适用于 UTM)
  312. if geodetic_datum == "China_2000":
  313. if projection.__contains__("-degree"):
  314. number = projection.split("-degree")[0][-1:]
  315. divided_into_zones = f'{number}度带'
  316. with_number = int(central_meridian / int(number))
  317. elif spatial_ref.IsProjected() and "UTM" in projection:
  318. zone = spatial_ref.GetUTMZone()
  319. divided_into_zones = "UTM分带" if zone else "其他"
  320. with_number = zone if zone else "N/A"
  321. # TODO 平面坐标单位
  322. plane_coordinate_unit = spatial_ref.GetLinearUnitsName()
  323. linear_units = spatial_ref.GetLinearUnits() # 单位与米的换算关系
  324. # 高程系统(如果有)
  325. altitude_name = spatial_ref.GetAttrValue("VERT_CS")
  326. altitude_benchmark = spatial_ref.GetAttrValue("VERT_DATUM")
  327. # 获取四向接边情况
  328. numlist = splittingnum.split(",")
  329. length = int(numlist[0].split(":")[1]) - int(numlist[0].split(":")[0])
  330. imagex = int(image_number[int(numlist[0].split(":")[0]):int(numlist[0].split(":")[1])])
  331. imagey = int(image_number[int(numlist[1].split(":")[0]):int(numlist[1].split(":")[1])])
  332. splitting = self.getSplitting(image_number, image_format, imagex, imagey, length, folder_path,
  333. selectfeature)
  334. metadatadict = {
  335. "image_number": image_number,
  336. "data_format": image_format,
  337. "data_volume": file_size,
  338. "grid_size": grid_size,
  339. "grid_row": grid_row,
  340. "grid_col": grid_col,
  341. "decimal_number": decimal_number,
  342. "nodata_number": ",".join(map(str, nodata_list)),
  343. "cornerpoint_lon": self.decimal_to_dms(cornerpoint["lon"]),
  344. "cornerpoint_lat": self.decimal_to_dms(cornerpoint["lat"]),
  345. "cornerpoint_x": cornerpoint["x"],
  346. "cornerpoint_y": cornerpoint["y"],
  347. "ellipsoid_radius": ellipsoid_radius,
  348. "ellipsoid_flat": ellipsoid_flat,
  349. "geodetic_datum": "2000国家大地坐标系" if geodetic_datum == "China_2000" else geodetic_datum,
  350. "projection_name": "高斯-克吕格投影" if projection_name == "Transverse_Mercator" else projection_name,
  351. "central_meridian": central_meridian,
  352. "divided_into_zones": divided_into_zones,
  353. "with_number": with_number,
  354. "plane_coordinate_unit": "米" if plane_coordinate_unit == "metre" else plane_coordinate_unit,
  355. "altitude_name": altitude_name,
  356. "altitude_benchmark": altitude_benchmark,
  357. "join_edge_w": splitting["west"],
  358. "join_edge_e": splitting["east"],
  359. "join_edge_n": splitting["north"],
  360. "join_edge_s": splitting["south"],
  361. "land_forms": selectfeature["landforms"]
  362. }
  363. for field_name in selectfeature.fields().names():
  364. metadatadict[field_name] = selectfeature[field_name]
  365. print("栅格数据参数获取完成")
  366. # TODO 空间叠加
  367. for key in overlapParams:
  368. value = overlapParams[key]
  369. largest_feature = self.get_largest_overlapping_feature(raster_path=filepath,
  370. shp_path=overlapVectorPath)
  371. field_value = largest_feature[value]
  372. metadatadict[key] = field_value
  373. print("开始生成报告")
  374. # print(metadatadict)
  375. # 利用模板生成excel
  376. workbook = openpyxl.load_workbook(templatepath)
  377. print(f"{templatepath}打开成功")
  378. sheet = workbook.active
  379. print(f"sheet打开成功")
  380. # 定义替换数据的字典
  381. replacements = {}
  382. for attr in metadatadict:
  383. key = f"<{attr}>"
  384. value = metadatadict[attr]
  385. replacements[key] = value
  386. # 遍历单元格,替换占位符
  387. for row in sheet.iter_rows():
  388. for cell in row:
  389. if isinstance(cell.value, str): # 确保单元格内容是字符串
  390. for placeholder, replacement in replacements.items():
  391. if placeholder in cell.value:
  392. # 替换占位符
  393. try:
  394. cell.value = cell.value.replace(placeholder, str(replacement))
  395. except SomeException as e:
  396. print(e)
  397. # 保存修改后的文件
  398. print(output_path)
  399. workbook.save(f'{output_path}\\{image_number}.xlsx')
  400. totalNumber = totalNumber + 1
  401. print(f"元数据生成完成:{image_number}")
  402. print(f"共生成{totalNumber}个元数据表。")
  403. return {
  404. "状态": "成功",
  405. "任务": f"共生成{totalNumber}个元数据表。"
  406. }