You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

convert-svg-to-geotiff.py 12 KiB

3 年之前
3 年之前
3 年之前
3 年之前
3 年之前
3 年之前
3 年之前
3 年之前
3 年之前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import os
  2. import argparse
  3. import lxml.etree as ET
  4. import subprocess
  5. import flirimageextractor
  6. import cv2
  7. import numpy as np
  8. from pathlib import Path
  9. from selenium import webdriver
  10. import rasterio
  11. # import shapefile
  12. arg_parser = argparse.ArgumentParser(description='Export SVG composition of FLIR images as TIFF with thermo layer')
  13. arg_parser.add_argument('Input',
  14. metavar='input_svg',
  15. type=str,
  16. help='Path to the input SVG file cotaining xlinks to FLIR images')
  17. arg_parser.add_argument('Output',
  18. metavar='output_tiff',
  19. type=str,
  20. help='Output filename')
  21. args = arg_parser.parse_args()
  22. dirname = os.path.dirname(__file__)
  23. INPUT_PATH = os.path.join(dirname, args.Input)
  24. INPUT_DIR = os.path.split(INPUT_PATH)[0]
  25. TEMP_MAP_THERMALPNG_SVG_PATH = os.path.join(INPUT_DIR, 'map_thermalpng.svg')
  26. TEMP_MAP_THERMALPNG_PATH = os.path.join(INPUT_DIR, 'map_thermalpng.png')
  27. TEMP_MAP_PREVIEW_PATH = os.path.join(INPUT_DIR, 'map_preview.png')
  28. TEMP_MAP_UNGROUPED_SVG_PATH = os.path.join(INPUT_DIR, 'map_ungrouped.svg')
  29. TEMP_MAP_ALIGNMENTPROOF_PATH = os.path.join(INPUT_DIR, 'map_thermalpng_proof.png')
  30. THERMALPNG_DIR = 'thermalpngs'
  31. # VECTOR_SHAPEFILE_PATH = os.path.join(INPUT_DIR, 'shapefile')
  32. OUTPUT_PATH = os.path.join(dirname, args.Output)
  33. def make_thermalpng_tiles():
  34. """
  35. Extract thermal infomration as greyscale PNG-16
  36. (temp * 1000 to retain some decimals)
  37. and save the png tiles.
  38. Assuming, that the data will always have a positive value.
  39. """
  40. print('Building PNG tiles representing the thermal data ...')
  41. Path(os.path.join(INPUT_DIR, THERMALPNG_DIR)).mkdir(parents=True, exist_ok=True)
  42. png_output_dir = os.path.join(INPUT_DIR, THERMALPNG_DIR)
  43. for root_path, directories, file in os.walk(os.path.join(dirname, INPUT_DIR)):
  44. for file in file:
  45. if(file.endswith(".jpg")):
  46. print(' Processing ' + file)
  47. full_filepath = os.path.join(root_path, file)
  48. flir = flirimageextractor.FlirImageExtractor()
  49. flir.process_image(full_filepath)
  50. thermal_img_np = flir.thermal_image_np
  51. multiplied_image = cv2.multiply(thermal_img_np, 1000)
  52. output_file_path = os.path.join(png_output_dir, file + '.thermal.png')
  53. cv2.imwrite(output_file_path, multiplied_image.astype(np.uint16))
  54. def make_thermalpng_svg():
  55. """
  56. replaces the image paths with the thermal pngs
  57. and creates new SVG file
  58. """
  59. print('Replacing the images inside the SVG with the PNG tiles ...')
  60. # print("svg_file")
  61. # print(dir(svg_file))
  62. tree = ET.parse(INPUT_PATH)
  63. root = tree.getroot()
  64. # print(ET.tostring(root))
  65. # tile_rows = root.xpath('//image', namespaces={'n': "http://www.w3.org/2000/svg"})
  66. # print(dir(root))
  67. tile_elements = root.xpath('//*[@class="thermal_image"]')
  68. linkattrib ='{http://www.w3.org/1999/xlink}href'
  69. for tile in tile_elements:
  70. tile.attrib[linkattrib] = os.path.join(THERMALPNG_DIR, tile.attrib[linkattrib] + '.thermal.png')
  71. # Post Production
  72. # tile.attrib["mask"] = 'url(#tilefademask)'
  73. # tile.attrib["style"] = 'opacity:.7'
  74. # newxml = ET.tostring(tree, encoding="unicode")
  75. # print(newxml)
  76. # return newxml
  77. with open(TEMP_MAP_THERMALPNG_SVG_PATH, 'wb') as f:
  78. tree.write(f, encoding='utf-8')
  79. return tree
  80. def make_thermalpng():
  81. """
  82. exports the SVG canvas as Gray_16 PNG
  83. """
  84. print('Creating a big 16-bit grayscale PNG image representing the thermal data out of the SVG file ...')
  85. command = [
  86. 'inkscape',
  87. '--pipe',
  88. '--export-type=png',
  89. '--export-png-color-mode=Gray_16'
  90. ],
  91. input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb")
  92. output_file = open(TEMP_MAP_THERMALPNG_PATH, "wb")
  93. completed = subprocess.run(
  94. *command,
  95. cwd=INPUT_DIR, # needed for reative image links
  96. stdin=input_file,
  97. stdout=output_file
  98. )
  99. return completed
  100. # def make_thermalpreview():
  101. # """
  102. # exports the preview image
  103. # """
  104. # command = [
  105. # '/snap/bin/inkscape',
  106. # '--pipe',
  107. # '--export-type=png',
  108. # '--export-png-color-mode=Gray_8'
  109. # ],
  110. # input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb")
  111. # output_file = open(TEMP_MAP_PREVIEW_PATH, "wb")
  112. # completed = subprocess.run(
  113. # *command,
  114. # cwd=INPUT_DIR, # needed for reative image links
  115. # stdin=input_file,
  116. # stdout=output_file
  117. # )
  118. # return completed
  119. def get_thermal_numpy_array():
  120. print('Converting the PNG into NumPy Array and normalize temperature values ...')
  121. image = cv2.imread(TEMP_MAP_THERMALPNG_PATH, cv2.IMREAD_ANYDEPTH)
  122. image_float = image.astype(np.float32)
  123. image_float_normalized = cv2.divide(image_float, 1000)
  124. # print(image_float_normalized[1000][905]) # looking what's the value of some pixel
  125. return image_float_normalized
  126. def get_used_tiles_relpaths():
  127. """
  128. outputs an array of all used tile filenames in the input SVG
  129. (relative filepaths like they appear in the svg.)
  130. """
  131. images = []
  132. tree = ET.parse(INPUT_PATH)
  133. root = tree.getroot()
  134. tile_elements = root.xpath('//*[@class="thermal_image"]')
  135. linkattrib ='{http://www.w3.org/1999/xlink}href'
  136. for tile in tile_elements:
  137. images.append(tile.attrib[linkattrib])
  138. return images
  139. # def deg_coordinates_to_decimal(coordStr):
  140. # coordArr = coordStr.split(', ')
  141. # calculatedCoordArray = []
  142. # for calculation in coordArr:
  143. # calculationArr = calculation.split('/')
  144. # calculatedCoordArray.append(int(calculationArr[0]) / int(calculationArr[1]))
  145. # degrees = calculatedCoordArray[0]
  146. # minutes = calculatedCoordArray[1]
  147. # seconds = calculatedCoordArray[2]
  148. # decimal = (degrees + (minutes * 1/60) + (seconds * 1/60 * 1/60))
  149. # # print(decimal)
  150. # return decimal
  151. # def read_coordinates_from_tile(filename):
  152. # full_filepath = os.path.join(INPUT_DIR, filename)
  153. # with Image(filename=full_filepath) as image:
  154. # for key, value in image.metadata.items():
  155. # if key == 'exif:GPSLatitude':
  156. # # print('latstr', value)
  157. # lat = deg_coordinates_to_decimal(value) # lat -> Y vertical
  158. # if key == 'exif:GPSLongitude':
  159. # # print('lonstr', value)
  160. # lon = deg_coordinates_to_decimal(value) # lon -> X horizontal
  161. # return [lat, lon]
  162. # def get_coordinate_boundaries():
  163. # image_names = get_used_tiles_relpaths()
  164. # coordinates = {
  165. # 'lat': [],
  166. # 'lon': []
  167. # }
  168. # for filename in image_names:
  169. # tile_coordinates = read_coordinates_from_tile(filename)
  170. # coordinates['lat'].append(tile_coordinates[0])
  171. # coordinates['lon'].append(tile_coordinates[1])
  172. # boundaries = {
  173. # 'xmin': min(coordinates['lon']),
  174. # 'xmax': max(coordinates['lon']),
  175. # 'ymin': min(coordinates['lat']),
  176. # 'ymax': max(coordinates['lat']),
  177. # }
  178. # return boundaries
  179. # def create_ungrouped_svg():
  180. # """
  181. # exports the SVG without any grouped elements
  182. # (the quick and dirty way)
  183. # """
  184. # print('Create an SVG without groups ...')
  185. # inkscape_actions = "select-all:groups; SelectionUnGroup; select-all:groups; SelectionUnGroup; select-all:groups; SelectionUnGroup; select-all:groups; SelectionUnGroup; select-all:groups; SelectionUnGroup; select-all:groups; SelectionUnGroup; select-all:groups; SelectionUnGroup; export-filename: {}; export-plain-svg; export-do;".format(TEMP_MAP_UNGROUPED_SVG_PATH)
  186. # command = [
  187. # '/snap/bin/inkscape',
  188. # '--pipe',
  189. # '--actions={}'.format(inkscape_actions),
  190. # ],
  191. # input_file = open(INPUT_PATH, "rb")
  192. # completed = subprocess.run(
  193. # *command,
  194. # cwd=INPUT_DIR, # needed for reative image links
  195. # stdin=input_file,
  196. # )
  197. # print('completed', completed)
  198. # return completed
  199. def get_ground_control_points():
  200. """
  201. Using selenium with firefox for rendering the SVG and
  202. getting the positional matching between GPS
  203. and x/y coords of SVG-image.
  204. image tags need to have the gps data attached as data attributes.
  205. """
  206. print('Getting ground control points ...')
  207. options = webdriver.firefox.options.Options()
  208. options.headless = True
  209. driver = webdriver.Firefox(options=options)
  210. driver.get("file://{}".format(INPUT_PATH))
  211. images = driver.find_elements_by_class_name("thermal_image")
  212. gcps = []
  213. for image in images:
  214. location = image.location
  215. size = image.size
  216. raster_y = float(location['y'] + size['height']/2)
  217. raster_x = float(location['x'] + size['width']/2)
  218. reference_lon = float(image.get_attribute('data-lon'))
  219. reference_lat = float(image.get_attribute('data-lat'))
  220. imageMapping = rasterio.control.GroundControlPoint(row=raster_y, col=raster_x, x=reference_lon, y=reference_lat)
  221. gcps.append(imageMapping)
  222. driver.quit()
  223. return gcps
  224. # def make_vector_shapefile():
  225. # w = shapefile.Writer(VECTOR_SHAPEFILE_PATH)
  226. # w.field('name', 'C')
  227. # tree = ET.parse(INPUT_PATH)
  228. # root = tree.getroot()
  229. # tiles = root.xpath('//*[@class="thermal_image"]')
  230. # for index, tile in enumerate(tiles):
  231. # w.point(float(tile.attrib['data-lon']), float(tile.attrib['data-lat']))
  232. # w.record('point{}'.format(index))
  233. # w.close()
  234. # return True
  235. def verify_coordinate_matching():
  236. """
  237. During development, i want to proof
  238. that the point/coordinate matching is right.
  239. Producing a png file with red dots for visual proof.
  240. """
  241. img = cv2.imread(TEMP_MAP_THERMALPNG_PATH, cv2.IMREAD_GRAYSCALE)
  242. img = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB)
  243. options = webdriver.firefox.options.Options()
  244. # options.headless = True
  245. driver = webdriver.Firefox(options=options)
  246. driver.get("file://{}".format(INPUT_PATH))
  247. images = driver.find_elements_by_class_name("thermal_image")
  248. gcps = []
  249. for image in images:
  250. location = image.location
  251. size = image.size
  252. raster_y = int(location['y'] + size['height']/2)
  253. raster_x = int(location['x'] + size['width']/2)
  254. reference_lon = float(image.get_attribute('data-lon'))
  255. reference_lat = float(image.get_attribute('data-lat'))
  256. print(raster_x, raster_y)
  257. img = cv2.circle(img, (raster_x, raster_y), radius=10, color=(0, 0, 255), thickness=-1)
  258. driver.quit()
  259. cv2.imwrite(TEMP_MAP_ALIGNMENTPROOF_PATH, img)
  260. def make_geotiff_image():
  261. thermal_numpy_array = get_thermal_numpy_array()
  262. thermal_numpy_array[thermal_numpy_array == 0] = np.NaN # zeros to NaN
  263. # # coordinates of all tiles
  264. # geo_bound = get_coordinate_boundaries()
  265. # print('boundaries', geo_bound)
  266. np_shape = thermal_numpy_array.shape
  267. image_size = (np_shape[0], np_shape[1])
  268. gcps = get_ground_control_points()
  269. print('Applying affine transform ...')
  270. gcp_transform = rasterio.transform.from_gcps(gcps)
  271. print(gcp_transform)
  272. print('Generating the GeoTiff ...')
  273. raster_io_dataset = rasterio.open(
  274. OUTPUT_PATH,
  275. 'w',
  276. driver='GTiff',
  277. height=thermal_numpy_array.shape[0],
  278. width=thermal_numpy_array.shape[1],
  279. count=1,
  280. dtype=thermal_numpy_array.dtype,
  281. transform=gcp_transform,
  282. crs='+proj=latlong',
  283. )
  284. raster_io_dataset.write(thermal_numpy_array, 1)
  285. # # try to get rid of the black frame
  286. # src = rasterio.open(OUTPUT_PATH)
  287. # src[src == 0] = np.NaN # zeros to NaN
  288. # raster_io_dataset2 = rasterio.open(
  289. # OUTPUT_PATH,
  290. # 'w',
  291. # driver='GTiff',
  292. # height=src.shape[0],
  293. # width=src.shape[1],
  294. # count=1,
  295. # dtype=src.dtype,
  296. # crs='+proj=latlong',
  297. # )
  298. # raster_io_dataset2.write(src, 1)
  299. print('Saved to ', OUTPUT_PATH)
  300. # make_thermalpng_tiles()
  301. # make_thermalpng_svg()
  302. # make_thermalpng()
  303. # make_geotiff_image()
  304. # # Helpers for debugging
  305. verify_coordinate_matching()
  306. # make_vector_shapefile()