makeMetadate.py 20 KB

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