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.

373 lines
13 KiB

  1. import os
  2. import argparse
  3. from wand.image import Image
  4. import lxml.etree as ET
  5. import copy
  6. import math
  7. from pyproj import CRS
  8. from pyproj.aoi import AreaOfInterest
  9. from pyproj.database import query_utm_crs_info
  10. from pyproj import Transformer
  11. arg_parser = argparse.ArgumentParser(description='Place drone FLIR-tiles into a SVG in order to edit them in Inkscape')
  12. arg_parser.add_argument('Input',
  13. metavar='input_directory',
  14. type=str,
  15. help='Path where the FLIR tiles are')
  16. arg_parser.add_argument(
  17. '--scale', action='store', default=15,
  18. help="Scaling (higher number leads to bigger canvas and less dense tiles) (defaults to 15)",
  19. type=int, dest='scale'
  20. )
  21. arg_parser.add_argument(
  22. '--direction', action='store', default='both',
  23. help="left, right, both (both is default)",
  24. type=str, dest='direction'
  25. )
  26. arg_parser.add_argument(
  27. '--base_rotation', action='store', default=115,
  28. help="Base orientation of drone in degrees (0-360) Defaults to 115",
  29. type=int, dest='base_rotation'
  30. )
  31. arg_parser.add_argument(
  32. '--rotation_corr_right', action='store', default=0,
  33. help="rotation correction for tiles where drone flies into right direction",
  34. type=int, dest='rotation_corr_right'
  35. )
  36. arg_parser.add_argument(
  37. '--rotation_corr_left', action='store', default=0,
  38. help="rotation correction for tiles where drone flies into left direction",
  39. type=int, dest='rotation_corr_left'
  40. )
  41. arg_parser.add_argument(
  42. '--pos_corr_right', action='store', default="0x0",
  43. help="position correction for tiles where drone flies into right direction in pixels. eg. 10x80",
  44. type=str, dest='pos_corr_right'
  45. )
  46. arg_parser.add_argument(
  47. '--pos_corr_left', action='store', default="0x0",
  48. help="position correction for tiles where drone flies into left direction in pixels. eg. 10x80",
  49. type=str, dest='pos_corr_left'
  50. )
  51. args = arg_parser.parse_args()
  52. dirname = os.path.dirname(__file__)
  53. working_dir = args.Input
  54. OUTPUT_PATH = os.path.join(working_dir,'map.svg')
  55. filename = os.path.join(dirname, 'canvas.svg')
  56. tree = ET.parse(filename)
  57. root = tree.getroot()
  58. d = root.nsmap
  59. main_layer = root.xpath('//*[@id="tiles"]', namespaces={'n': "http://www.w3.org/2000/svg"})[0]
  60. tile_rows = root.xpath('//*[@id="tile_rows"]', namespaces={'n': "http://www.w3.org/2000/svg"})[0]
  61. def deg_coordinates_to_decimal(coordStr):
  62. coordArr = value.split(', ')
  63. calculatedCoordArray = []
  64. for calculation in coordArr:
  65. calculationArr = calculation.split('/')
  66. calculatedCoordArray.append(int(calculationArr[0]) / int(calculationArr[1]))
  67. degrees = calculatedCoordArray[0]
  68. minutes = calculatedCoordArray[1]
  69. seconds = calculatedCoordArray[2]
  70. return (degrees + (minutes * 1/60) + (seconds * 1/60 * 1/60))
  71. # finding the boundaries of the whole canvas
  72. latsArr = []
  73. lonsArr = []
  74. for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)):
  75. for file in file:
  76. if(file.endswith(".jpg")):
  77. # print(os.path.join(root_path, file))
  78. full_filepath = os.path.join(root_path, file)
  79. with Image(filename=full_filepath) as image:
  80. # print(image.width)
  81. # print(image.height)
  82. for key, value in image.metadata.items():
  83. if key == 'exif:GPSLatitude':
  84. lat = deg_coordinates_to_decimal(value) # lat -> Y vertical
  85. latsArr.append(lat)
  86. # print("{}: {}".format(key, value))
  87. # print('lat '+ str(lat))
  88. if key == 'exif:GPSLongitude':
  89. lon = deg_coordinates_to_decimal(value) # lon -> X horizontal
  90. lonsArr.append(lon)
  91. # print("{}: {}".format(key, value))
  92. # print('lon '+ str(lon))
  93. minLat = min(latsArr)
  94. minLon = min(lonsArr)
  95. maxLat = max(latsArr)
  96. maxLon = max(lonsArr)
  97. midLon = (minLon + maxLon) /2
  98. midLat = (minLat + maxLat) /2
  99. # find CRS system
  100. utm_crs_list = query_utm_crs_info(
  101. datum_name="WGS 84",
  102. area_of_interest=AreaOfInterest(
  103. west_lon_degree=minLon,
  104. south_lat_degree=minLat,
  105. east_lon_degree=maxLon,
  106. north_lat_degree=maxLat,
  107. ),
  108. )
  109. utm_crs = CRS.from_epsg(utm_crs_list[0].code)
  110. transformer = Transformer.from_crs("EPSG:4326", utm_crs, always_xy=True)
  111. min_transformed_lon, min_transformed_lat = transformer.transform(minLon, minLat)
  112. max_transformed_lon, max_transformed_lat = transformer.transform(maxLon, maxLat)
  113. width = max_transformed_lon - min_transformed_lon
  114. height = max_transformed_lat - min_transformed_lat
  115. # def latlngToGlobalXY(lat, lng):
  116. # earth_radius = 6371
  117. # # Calculates x based on cos of average of the latitudes
  118. # x = earth_radius * lng * math.cos((minLat + maxLat)/2)
  119. # # Calculates y based on latitude
  120. # y = earth_radius * lat
  121. # return {x: x, y: y}
  122. # def latlngToScreenXY(lat, lng):
  123. # topLeft_corner = latlngToGlobalXY(minLat, minLon)
  124. # bottomRight_corner = latlngToGlobalXY(maxLat, maxLon)
  125. # # Calculate global X and Y for projection point
  126. # pos = latlngToGlobalXY(lat, lng)
  127. # # Calculate the percentage of Global X position in relation to total global width
  128. # pos.perX = ((pos.x - topLeft_corner.x) / (bottomRight_corner.x - topLeft_corner.x))
  129. # # Calculate the percentage of Global Y position in relation to total global height
  130. # pos.perY = ((pos.y - topLeft_corner.y) / (bottomRight_corner.y - topLeft_corner.y))
  131. # # Returns the screen position based on reference points
  132. # return {
  133. # x: p0.scrX + (p1.scrX - p0.scrX)*pos.perX,
  134. # y: p0.scrY + (p1.scrY - p0.scrY)*pos.perY
  135. # }
  136. # placing the images into the svg
  137. # image_rotation_left = rotation #32
  138. # image_rotation_right = rotation + 180 #192
  139. for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)):
  140. for file in file:
  141. if(file.endswith(".jpg")):
  142. # print(os.path.join(root_path, file))
  143. full_filepath = os.path.join(root_path, file)
  144. with Image(filename=full_filepath) as image:
  145. # print(image.width)
  146. # print(image.height)
  147. for key, value in image.metadata.items():
  148. # print("{}: {}".format(key, value))
  149. if key == 'exif:GPSLatitude':
  150. lat = deg_coordinates_to_decimal(value)
  151. lat_offset = lat - minLat
  152. if key == 'exif:GPSLongitude':
  153. lon = deg_coordinates_to_decimal(value)
  154. lon_offset = lon - minLon
  155. if key == 'exif:GPSImgDirection':
  156. direction = value.split('/')
  157. rotation = ( int(direction[0]) / int(direction[1]) ) / 2 + args.base_rotation
  158. # print('rotation',rotation)
  159. transformed_lon, transformed_lat = transformer.transform(lon, lat)
  160. lon_offset = transformed_lon - min_transformed_lon
  161. lat_offset = transformed_lat - min_transformed_lat
  162. # print(transformed_lon, min_transformed_lon, transformed_lat, min_transformed_lat)
  163. # print('lon_offset, lat_offset', lon_offset, lat_offset)
  164. g_pos_el_attributes = {
  165. 'transform': "translate({}, {})".format(format(lon_offset*args.scale, '.20f'), format(lat_offset*args.scale*-1, '.20f')),
  166. 'data-lat-offset': format(lat_offset, '.20f'),
  167. 'data-lon-offset': format(lon_offset, '.20f'),
  168. 'class': 'tile',
  169. 'id': 'tile_{}'.format(file.split('.')[0]),
  170. }
  171. g_pos_el = ET.SubElement(main_layer, 'g', attrib=g_pos_el_attributes)
  172. g_offset_corr_el_attributes = {
  173. 'data-offset-corr-x': "{}".format(-image.width/2),
  174. 'data-offset-corr-y': "{}".format(-image.height/2),
  175. 'transform': "translate({}, {})".format(-image.width/2, -image.height/2),
  176. 'class': 'tile-offset-corr',
  177. }
  178. g_offset_corr_el = ET.SubElement(g_pos_el, 'g', attrib=g_offset_corr_el_attributes)
  179. g_rot_el_attributes = {
  180. 'class': 'tile-rotate',
  181. 'data-image-rotation': str(rotation),
  182. 'data-image-dimensions': str(image.width) + ' ' + str(image.height),
  183. 'transform': 'rotate({} {} {})'.format(str(rotation), str(image.width/2), str(image.height/2))
  184. # 'transform': 'rotate({} {} {})'.format(str(rotation), 0,0)
  185. }
  186. g_rot_el = ET.SubElement(g_offset_corr_el, 'g', attrib=g_rot_el_attributes)
  187. xlinkns ="http://www.w3.org/1999/xlink"
  188. image_el = ET.SubElement(g_rot_el, 'image', {
  189. "class": 'thermal_image',
  190. "{%s}href" % xlinkns: file,
  191. "width": str(image.width),
  192. "height": str(image.height),
  193. 'data-lat': format(lat, '.20f'),
  194. 'data-lon': format(lon, '.20f'),
  195. })
  196. # sort elements
  197. def getkey(elem):
  198. # Used for sorting elements by @LIN.
  199. # returns a tuple of ints from the exploded @LIN value
  200. # '1.0' -> (1,0)
  201. # '1.0.1' -> (1,0,1)
  202. return float(elem.get('id').split('_')[2])
  203. main_layer[:] = sorted(main_layer, key=getkey)
  204. # find rows
  205. # left/right is actually left/right or right/left
  206. last_direction = 'right'
  207. for index, el in enumerate(main_layer):
  208. if(el.getprevious() is not None):
  209. prev_lon_offset = el.getprevious().attrib['data-lon-offset']
  210. lon_offset = el.attrib['data-lon-offset']
  211. # determine direction
  212. direction = None
  213. # print(prev_lon_offset - lon_offset)
  214. if (prev_lon_offset == lon_offset):
  215. direction = last_direction
  216. # print('same lon!')
  217. elif (float(prev_lon_offset) > float(lon_offset)):
  218. direction = 'left'
  219. elif (float(prev_lon_offset) < float(lon_offset)):
  220. direction = 'right'
  221. if (direction == 'left'):
  222. # print('left {} | {} -> {}'.format(el.attrib['id'], prev_lon_offset, lon_offset))
  223. el.attrib['data-direction'] = 'left'
  224. if (args.pos_corr_left):
  225. offset_corr_x, offset_corr_y = args.pos_corr_left.split('x')
  226. # print(offset_corr_x, offset_corr_y)
  227. pos_corr_el = el[0]
  228. corrected_pos_x = float(pos_corr_el.attrib['data-offset-corr-x']) + float(offset_corr_x)
  229. corrected_pos_y = float(pos_corr_el.attrib['data-offset-corr-y']) + float(offset_corr_y)
  230. pos_corr_el.attrib['transform'] = "translate({}px, {}px)".format(corrected_pos_x, corrected_pos_y)
  231. if (args.rotation_corr_left):
  232. rot_el = el[0][0]
  233. corrected_rotation = float(rot_el.attrib['data-image-rotation']) + args.rotation_corr_left
  234. rot_el.attrib['data-image-rotation'] = str(corrected_rotation)
  235. image = rot_el[0]
  236. rot_el.attrib['transform'] = 'rotate({} {} {})'.format(str(corrected_rotation), str(int(image.attrib['width'])/2), str(int(image.attrib['height'])/2))
  237. if (direction == 'right'):
  238. # print('right {} | {} -> {}'.format(el.attrib['id'], prev_lon_offset, lon_offset))
  239. el.attrib['data-direction'] = 'right'
  240. if (args.pos_corr_right):
  241. offset_corr_x, offset_corr_y = args.pos_corr_right.split('x')
  242. # print(offset_corr_x, offset_corr_y)
  243. pos_corr_el = el[0]
  244. corrected_pos_x = float(pos_corr_el.attrib['data-offset-corr-x']) + float(offset_corr_x)
  245. corrected_pos_y = float(pos_corr_el.attrib['data-offset-corr-y']) + float(offset_corr_y)
  246. pos_corr_el.attrib['transform'] = "translate({}px, {}px)".format(corrected_pos_x, corrected_pos_y)
  247. if (args.rotation_corr_right):
  248. rot_el = el[0][0]
  249. corrected_rotation = float(rot_el.attrib['data-image-rotation']) + args.rotation_corr_right
  250. rot_el.attrib['data-image-rotation'] = str(corrected_rotation)
  251. image = rot_el[0]
  252. rot_el.attrib['transform'] = 'rotate({} {} {})'.format(str(corrected_rotation), str(int(image.attrib['width'])/2), str(int(image.attrib['height'])/2))
  253. # merge tiles into groups
  254. # Start new line
  255. if (args.direction == 'both' or args.direction == el.attrib['data-direction']):
  256. copyElem = copy.deepcopy(el)
  257. # if direction changes
  258. if (index == 1 or last_direction != el.attrib['data-direction']):
  259. # print('new row!')
  260. current_row = ET.SubElement(tile_rows, 'g', attrib={ 'class': 'tile-row' })
  261. # print (el.attrib['id'], el.attrib['data-direction'])
  262. current_row.insert(0, copyElem)
  263. last_direction = el.attrib['data-direction']
  264. # remove temporary group
  265. root.remove(main_layer)
  266. #
  267. # resize canvas to tiles and add some padding
  268. # print(width, height, args.scale)
  269. scaled_width = width * args.scale
  270. scaled_height = height * args.scale
  271. padding = 500
  272. canvas_width = str(scaled_width + padding*2)
  273. canvas_height = str(scaled_height + padding*2)
  274. viewbox_x = str(padding * -1)
  275. viewbox_y = str((scaled_height + padding) * -1)
  276. viewbox_width = canvas_width
  277. viewbox_height = canvas_height
  278. root.attrib['width'] = canvas_width
  279. root.attrib['height'] = canvas_height
  280. root.attrib['viewBox'] = "{} {} {} {}".format(viewbox_x, viewbox_y, viewbox_width, viewbox_height)
  281. # Finally save the svg
  282. with open(OUTPUT_PATH, 'wb') as f:
  283. tree.write(f, encoding='utf-8')
  284. print('Saved SVG to {}'.format(OUTPUT_PATH))