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.

386 lines
12 KiB

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