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.

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