| @@ -19,9 +19,6 @@ | |||||
| xmlns:cc="http://creativecommons.org/ns#" | xmlns:cc="http://creativecommons.org/ns#" | ||||
| xmlns:dc="http://purl.org/dc/elements/1.1/"> | xmlns:dc="http://purl.org/dc/elements/1.1/"> | ||||
| <style> | <style> | ||||
| .thermal_image { | |||||
| opacity: .5 ; | |||||
| } | |||||
| g.tile[data-direction='down'] { | g.tile[data-direction='down'] { | ||||
| opacity: 1; | opacity: 1; | ||||
| } | } | ||||
| @@ -30,7 +27,7 @@ g.tile[data-direction='down'] { | |||||
| } --> | } --> | ||||
| </style> | </style> | ||||
| <defs id="defs2"> | <defs id="defs2"> | ||||
| <!-- <filter | |||||
| <filter | |||||
| xmlns="http://www.w3.org/2000/svg" | xmlns="http://www.w3.org/2000/svg" | ||||
| style="color-interpolation-filters:sRGB;" | style="color-interpolation-filters:sRGB;" | ||||
| inkscape:label="Blur" | inkscape:label="Blur" | ||||
| @@ -56,7 +53,7 @@ g.tile[data-direction='down'] { | |||||
| y="-25" | y="-25" | ||||
| style="fill:#ffffff;filter:url(#filter156128)" | style="fill:#ffffff;filter:url(#filter156128)" | ||||
| transform="matrix(0.77703373,0,0,0.74018882,207.24036,100.70559)" /> | transform="matrix(0.77703373,0,0,0.74018882,207.24036,100.70559)" /> | ||||
| </mask> --> | |||||
| </mask> | |||||
| </defs> | </defs> | ||||
| <sodipodi:namedview | <sodipodi:namedview | ||||
| @@ -0,0 +1,378 @@ | |||||
| import os | |||||
| import argparse | |||||
| import lxml.etree as ET | |||||
| import subprocess | |||||
| import flirimageextractor | |||||
| import cv2 | |||||
| import numpy as np | |||||
| from pathlib import Path | |||||
| from selenium import webdriver | |||||
| import rasterio | |||||
| # import shapefile | |||||
| arg_parser = argparse.ArgumentParser(description='Export SVG composition of FLIR images as TIFF with thermo layer') | |||||
| arg_parser.add_argument('Input', | |||||
| metavar='input_svg', | |||||
| type=str, | |||||
| help='Path to the input SVG file cotaining xlinks to FLIR images') | |||||
| arg_parser.add_argument('Output', | |||||
| metavar='output_tiff', | |||||
| type=str, | |||||
| help='Output filename') | |||||
| args = arg_parser.parse_args() | |||||
| dirname = os.path.dirname(__file__) | |||||
| INPUT_PATH = os.path.join(dirname, args.Input) | |||||
| INPUT_DIR = os.path.split(INPUT_PATH)[0] | |||||
| TEMP_MAP_THERMALPNG_SVG_PATH = os.path.join(INPUT_DIR, 'map_thermalpng.svg') | |||||
| TEMP_MAP_THERMALPNG_PATH = os.path.join(INPUT_DIR, 'map_thermalpng.png') | |||||
| TEMP_MAP_PREVIEW_PATH = os.path.join(INPUT_DIR, 'map_preview.png') | |||||
| TEMP_MAP_UNGROUPED_SVG_PATH = os.path.join(INPUT_DIR, 'map_ungrouped.svg') | |||||
| TEMP_MAP_ALIGNMENTPROOF_PATH = os.path.join(INPUT_DIR, 'map_thermalpng_proof.png') | |||||
| THERMALPNG_DIR = 'thermalpngs' | |||||
| # VECTOR_SHAPEFILE_PATH = os.path.join(INPUT_DIR, 'shapefile') | |||||
| OUTPUT_PATH = os.path.join(dirname, args.Output) | |||||
| def make_thermalpng_tiles(): | |||||
| """ | |||||
| Extract thermal infomration as greyscale PNG-16 | |||||
| (temp * 1000 to retain some decimals) | |||||
| and save the png tiles. | |||||
| Assuming, that the data will always have a positive value. | |||||
| """ | |||||
| print('Building PNG tiles representing the thermal data ...') | |||||
| Path(os.path.join(INPUT_DIR, THERMALPNG_DIR)).mkdir(parents=True, exist_ok=True) | |||||
| png_output_dir = os.path.join(INPUT_DIR, THERMALPNG_DIR) | |||||
| for root_path, directories, file in os.walk(os.path.join(dirname, INPUT_DIR)): | |||||
| for file in file: | |||||
| if(file.endswith(".jpg")): | |||||
| print(' Processing ' + file) | |||||
| full_filepath = os.path.join(root_path, file) | |||||
| flir = flirimageextractor.FlirImageExtractor() | |||||
| flir.process_image(full_filepath) | |||||
| thermal_img_np = flir.thermal_image_np | |||||
| multiplied_image = cv2.multiply(thermal_img_np, 1000) | |||||
| output_file_path = os.path.join(png_output_dir, file + '.thermal.png') | |||||
| cv2.imwrite(output_file_path, multiplied_image.astype(np.uint16)) | |||||
| def make_thermalpng_svg(): | |||||
| """ | |||||
| replaces the image paths with the thermal pngs | |||||
| and creates new SVG file | |||||
| """ | |||||
| print('Replacing the images inside the SVG with the PNG tiles ...') | |||||
| # print("svg_file") | |||||
| # print(dir(svg_file)) | |||||
| tree = ET.parse(INPUT_PATH) | |||||
| root = tree.getroot() | |||||
| # print(ET.tostring(root)) | |||||
| # tile_rows = root.xpath('//image', namespaces={'n': "http://www.w3.org/2000/svg"}) | |||||
| # print(dir(root)) | |||||
| tile_elements = root.xpath('//*[@class="thermal_image"]') | |||||
| linkattrib ='{http://www.w3.org/1999/xlink}href' | |||||
| for tile in tile_elements: | |||||
| tile.attrib[linkattrib] = os.path.join(THERMALPNG_DIR, tile.attrib[linkattrib] + '.thermal.png') | |||||
| # Post Production | |||||
| tile.attrib["mask"] = 'url(#tilefademask)' | |||||
| tile.attrib["style"] = 'opacity:.7' | |||||
| # newxml = ET.tostring(tree, encoding="unicode") | |||||
| # print(newxml) | |||||
| # return newxml | |||||
| with open(TEMP_MAP_THERMALPNG_SVG_PATH, 'wb') as f: | |||||
| tree.write(f, encoding='utf-8') | |||||
| return tree | |||||
| def make_thermalpng(): | |||||
| """ | |||||
| exports the SVG canvas as Gray_16 PNG | |||||
| """ | |||||
| print('Creating a big 16-bit grayscale PNG image representing the thermal data out of the SVG file ...') | |||||
| command = [ | |||||
| '/snap/bin/inkscape', | |||||
| '--pipe', | |||||
| '--export-type=png', | |||||
| '--export-png-color-mode=Gray_16' | |||||
| ], | |||||
| input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb") | |||||
| output_file = open(TEMP_MAP_THERMALPNG_PATH, "wb") | |||||
| completed = subprocess.run( | |||||
| *command, | |||||
| cwd=INPUT_DIR, # needed for reative image links | |||||
| stdin=input_file, | |||||
| stdout=output_file | |||||
| ) | |||||
| return completed | |||||
| # def make_thermalpreview(): | |||||
| # """ | |||||
| # exports the preview image | |||||
| # """ | |||||
| # command = [ | |||||
| # '/snap/bin/inkscape', | |||||
| # '--pipe', | |||||
| # '--export-type=png', | |||||
| # '--export-png-color-mode=Gray_8' | |||||
| # ], | |||||
| # input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb") | |||||
| # output_file = open(TEMP_MAP_PREVIEW_PATH, "wb") | |||||
| # completed = subprocess.run( | |||||
| # *command, | |||||
| # cwd=INPUT_DIR, # needed for reative image links | |||||
| # stdin=input_file, | |||||
| # stdout=output_file | |||||
| # ) | |||||
| # return completed | |||||
| def get_thermal_numpy_array(): | |||||
| print('Converting the PNG into NumPy Array and normalize temperature values ...') | |||||
| image = cv2.imread(TEMP_MAP_THERMALPNG_PATH, cv2.IMREAD_ANYDEPTH) | |||||
| image_float = image.astype(np.float32) | |||||
| image_float_normalized = cv2.divide(image_float, 1000) | |||||
| # print(image_float_normalized[1000][905]) # looking what's the value of some pixel | |||||
| return image_float_normalized | |||||
| def get_used_tiles_relpaths(): | |||||
| """ | |||||
| outputs an array of all used tile filenames in the input SVG | |||||
| (relative filepaths like they appear in the svg.) | |||||
| """ | |||||
| images = [] | |||||
| tree = ET.parse(INPUT_PATH) | |||||
| root = tree.getroot() | |||||
| tile_elements = root.xpath('//*[@class="thermal_image"]') | |||||
| linkattrib ='{http://www.w3.org/1999/xlink}href' | |||||
| for tile in tile_elements: | |||||
| images.append(tile.attrib[linkattrib]) | |||||
| return images | |||||
| # def deg_coordinates_to_decimal(coordStr): | |||||
| # coordArr = coordStr.split(', ') | |||||
| # calculatedCoordArray = [] | |||||
| # for calculation in coordArr: | |||||
| # calculationArr = calculation.split('/') | |||||
| # calculatedCoordArray.append(int(calculationArr[0]) / int(calculationArr[1])) | |||||
| # degrees = calculatedCoordArray[0] | |||||
| # minutes = calculatedCoordArray[1] | |||||
| # seconds = calculatedCoordArray[2] | |||||
| # decimal = (degrees + (minutes * 1/60) + (seconds * 1/60 * 1/60)) | |||||
| # # print(decimal) | |||||
| # return decimal | |||||
| # def read_coordinates_from_tile(filename): | |||||
| # full_filepath = os.path.join(INPUT_DIR, filename) | |||||
| # with Image(filename=full_filepath) as image: | |||||
| # for key, value in image.metadata.items(): | |||||
| # if key == 'exif:GPSLatitude': | |||||
| # # print('latstr', value) | |||||
| # lat = deg_coordinates_to_decimal(value) # lat -> Y vertical | |||||
| # if key == 'exif:GPSLongitude': | |||||
| # # print('lonstr', value) | |||||
| # lon = deg_coordinates_to_decimal(value) # lon -> X horizontal | |||||
| # return [lat, lon] | |||||
| # def get_coordinate_boundaries(): | |||||
| # image_names = get_used_tiles_relpaths() | |||||
| # coordinates = { | |||||
| # 'lat': [], | |||||
| # 'lon': [] | |||||
| # } | |||||
| # for filename in image_names: | |||||
| # tile_coordinates = read_coordinates_from_tile(filename) | |||||
| # coordinates['lat'].append(tile_coordinates[0]) | |||||
| # coordinates['lon'].append(tile_coordinates[1]) | |||||
| # boundaries = { | |||||
| # 'xmin': min(coordinates['lon']), | |||||
| # 'xmax': max(coordinates['lon']), | |||||
| # 'ymin': min(coordinates['lat']), | |||||
| # 'ymax': max(coordinates['lat']), | |||||
| # } | |||||
| # return boundaries | |||||
| # def create_ungrouped_svg(): | |||||
| # """ | |||||
| # exports the SVG without any grouped elements | |||||
| # (the quick and dirty way) | |||||
| # """ | |||||
| # print('Create an SVG without groups ...') | |||||
| # 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) | |||||
| # command = [ | |||||
| # '/snap/bin/inkscape', | |||||
| # '--pipe', | |||||
| # '--actions={}'.format(inkscape_actions), | |||||
| # ], | |||||
| # input_file = open(INPUT_PATH, "rb") | |||||
| # completed = subprocess.run( | |||||
| # *command, | |||||
| # cwd=INPUT_DIR, # needed for reative image links | |||||
| # stdin=input_file, | |||||
| # ) | |||||
| # print('completed', completed) | |||||
| # return completed | |||||
| def get_ground_control_points(): | |||||
| """ | |||||
| Using selenium with firefox for rendering the SVG and | |||||
| getting the positional matching between GPS | |||||
| and x/y coords of SVG-image. | |||||
| image tags need to have the gps data attached as data attributes. | |||||
| """ | |||||
| print('Getting ground control points ...') | |||||
| options = webdriver.firefox.options.Options() | |||||
| options.headless = True | |||||
| driver = webdriver.Firefox(options=options) | |||||
| driver.get("file://{}".format(INPUT_PATH)) | |||||
| images = driver.find_elements_by_class_name("thermal_image") | |||||
| gcps = [] | |||||
| for image in images: | |||||
| location = image.location | |||||
| size = image.size | |||||
| raster_y = float(location['y'] + size['height']/2) | |||||
| raster_x = float(location['x'] + size['width']/2) | |||||
| reference_lon = float(image.get_attribute('data-lon')) | |||||
| reference_lat = float(image.get_attribute('data-lat')) | |||||
| imageMapping = rasterio.control.GroundControlPoint(row=raster_y, col=raster_x, x=reference_lon, y=reference_lat) | |||||
| gcps.append(imageMapping) | |||||
| driver.quit() | |||||
| return gcps | |||||
| # def make_vector_shapefile(): | |||||
| # w = shapefile.Writer(VECTOR_SHAPEFILE_PATH) | |||||
| # w.field('name', 'C') | |||||
| # tree = ET.parse(INPUT_PATH) | |||||
| # root = tree.getroot() | |||||
| # tiles = root.xpath('//*[@class="thermal_image"]') | |||||
| # for index, tile in enumerate(tiles): | |||||
| # w.point(float(tile.attrib['data-lon']), float(tile.attrib['data-lat'])) | |||||
| # w.record('point{}'.format(index)) | |||||
| # w.close() | |||||
| # return True | |||||
| def verify_coordinate_matching(): | |||||
| """ | |||||
| During development, i want to proof | |||||
| that the point/coordinate matching is right. | |||||
| Producing a png file with red dots for visual proof. | |||||
| """ | |||||
| img = cv2.imread(TEMP_MAP_THERMALPNG_PATH, cv2.IMREAD_GRAYSCALE) | |||||
| img = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB) | |||||
| options = webdriver.firefox.options.Options() | |||||
| # options.headless = True | |||||
| driver = webdriver.Firefox(options=options) | |||||
| driver.get("file://{}".format(INPUT_PATH)) | |||||
| images = driver.find_elements_by_class_name("thermal_image") | |||||
| gcps = [] | |||||
| for image in images: | |||||
| location = image.location | |||||
| size = image.size | |||||
| raster_y = int(location['y'] + size['height']/2) | |||||
| raster_x = int(location['x'] + size['width']/2) | |||||
| reference_lon = float(image.get_attribute('data-lon')) | |||||
| reference_lat = float(image.get_attribute('data-lat')) | |||||
| print(raster_x, raster_y) | |||||
| img = cv2.circle(img, (raster_x, raster_y), radius=10, color=(0, 0, 255), thickness=-1) | |||||
| driver.quit() | |||||
| cv2.imwrite(TEMP_MAP_ALIGNMENTPROOF_PATH, img) | |||||
| def make_geotiff_image(): | |||||
| thermal_numpy_array = get_thermal_numpy_array() | |||||
| thermal_numpy_array[thermal_numpy_array == 0] = np.NaN # zeros to NaN | |||||
| # # coordinates of all tiles | |||||
| # geo_bound = get_coordinate_boundaries() | |||||
| # print('boundaries', geo_bound) | |||||
| np_shape = thermal_numpy_array.shape | |||||
| image_size = (np_shape[0], np_shape[1]) | |||||
| gcps = get_ground_control_points() | |||||
| print('Applying affine transform ...') | |||||
| gcp_transform = rasterio.transform.from_gcps(gcps) | |||||
| print(gcp_transform) | |||||
| print('Generating the GeoTiff ...') | |||||
| raster_io_dataset = rasterio.open( | |||||
| OUTPUT_PATH, | |||||
| 'w', | |||||
| driver='GTiff', | |||||
| height=thermal_numpy_array.shape[0], | |||||
| width=thermal_numpy_array.shape[1], | |||||
| count=1, | |||||
| dtype=thermal_numpy_array.dtype, | |||||
| transform=gcp_transform, | |||||
| crs='+proj=latlong', | |||||
| ) | |||||
| raster_io_dataset.write(thermal_numpy_array, 1) | |||||
| # # try to get rid of the black frame | |||||
| # src = rasterio.open(OUTPUT_PATH) | |||||
| # src[src == 0] = np.NaN # zeros to NaN | |||||
| # raster_io_dataset2 = rasterio.open( | |||||
| # OUTPUT_PATH, | |||||
| # 'w', | |||||
| # driver='GTiff', | |||||
| # height=src.shape[0], | |||||
| # width=src.shape[1], | |||||
| # count=1, | |||||
| # dtype=src.dtype, | |||||
| # crs='+proj=latlong', | |||||
| # ) | |||||
| # raster_io_dataset2.write(src, 1) | |||||
| print('Saved to ', OUTPUT_PATH) | |||||
| # make_thermalpng_tiles() | |||||
| make_thermalpng_svg() | |||||
| make_thermalpng() | |||||
| make_geotiff_image() | |||||
| # # Helpers for debugging | |||||
| # verify_coordinate_matching() | |||||
| # make_vector_shapefile() | |||||
| @@ -1,263 +0,0 @@ | |||||
| import os | |||||
| import argparse | |||||
| import lxml.etree as ET | |||||
| import subprocess | |||||
| import flirimageextractor | |||||
| import cv2 | |||||
| import numpy as np | |||||
| from pathlib import Path | |||||
| from wand.image import Image | |||||
| from osgeo import gdal | |||||
| from osgeo import osr | |||||
| arg_parser = argparse.ArgumentParser(description='Export SVG composition of FLIR images as TIFF with thermo layer') | |||||
| arg_parser.add_argument('Input', | |||||
| metavar='input_svg', | |||||
| type=str, | |||||
| help='Path to the input SVG file cotaining xlinks to FLIR images') | |||||
| arg_parser.add_argument('Output', | |||||
| metavar='output_tiff', | |||||
| type=str, | |||||
| help='Output filename') | |||||
| args = arg_parser.parse_args() | |||||
| dirname = os.path.dirname(__file__) | |||||
| INPUT_PATH = os.path.join(dirname, args.Input) | |||||
| INPUT_DIR = os.path.split(INPUT_PATH)[0] | |||||
| TEMP_MAP_THERMALPNG_SVG_PATH = os.path.join(INPUT_DIR, 'map_thermalpng.svg') | |||||
| TEMP_MAP_THERMALPNG_PATH = os.path.join(INPUT_DIR, 'map_thermalpng.png') | |||||
| TEMP_MAP_PREVIEW_PATH = os.path.join(INPUT_DIR, 'map_preview.png') | |||||
| THERMALPNG_DIR = 'thermalpngs' | |||||
| OUTPUT_PATH = os.path.join(dirname, args.Output) | |||||
| def make_thermalpng_tiles(): | |||||
| """ | |||||
| Extract thermal infomration as greyscale PNG-16 (temp * 1000 to retain some decimals) | |||||
| and save the png tiles | |||||
| """ | |||||
| Path(os.path.join(INPUT_DIR, THERMALPNG_DIR)).mkdir(parents=True, exist_ok=True) | |||||
| png_output_dir = os.path.join(INPUT_DIR, THERMALPNG_DIR) | |||||
| for root_path, directories, file in os.walk(os.path.join(dirname, INPUT_DIR)): | |||||
| for file in file: | |||||
| if(file.endswith(".jpg")): | |||||
| print('Extracting thermal info from ' + file) | |||||
| full_filepath = os.path.join(root_path, file) | |||||
| flir = flirimageextractor.FlirImageExtractor() | |||||
| flir.process_image(full_filepath) | |||||
| thermal_img_np = flir.thermal_image_np | |||||
| multiplied_image = cv2.multiply(thermal_img_np, 1000) | |||||
| output_file_path = os.path.join(png_output_dir, file + '.thermal.png') | |||||
| print(output_file_path) | |||||
| cv2.imwrite(output_file_path, multiplied_image.astype(np.uint16)) | |||||
| def make_thermalpng_svg(): | |||||
| """ | |||||
| replaces the image paths with the thermal pngs | |||||
| and creates new SVG file | |||||
| """ | |||||
| # print("svg_file") | |||||
| # print(dir(svg_file)) | |||||
| tree = ET.parse(INPUT_PATH) | |||||
| root = tree.getroot() | |||||
| # print(ET.tostring(root)) | |||||
| # tile_rows = root.xpath('//image', namespaces={'n': "http://www.w3.org/2000/svg"}) | |||||
| # print(dir(root)) | |||||
| tile_elements = root.xpath('//*[@class="thermal_image"]') | |||||
| linkattrib ='{http://www.w3.org/1999/xlink}href' | |||||
| for tile in tile_elements: | |||||
| tile.attrib[linkattrib] = os.path.join(THERMALPNG_DIR, tile.attrib[linkattrib] + '.thermal.png') | |||||
| # newxml = ET.tostring(tree, encoding="unicode") | |||||
| # print(newxml) | |||||
| # return newxml | |||||
| with open(TEMP_MAP_THERMALPNG_SVG_PATH, 'wb') as f: | |||||
| tree.write(f, encoding='utf-8') | |||||
| return tree | |||||
| def make_thermalpng(): | |||||
| """ | |||||
| exports the SVG canvas as Gray_16 PNG | |||||
| """ | |||||
| command = [ | |||||
| '/snap/bin/inkscape', | |||||
| '--pipe', | |||||
| '--export-type=png', | |||||
| '--export-png-color-mode=Gray_16' | |||||
| ], | |||||
| input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb") | |||||
| output_file = open(TEMP_MAP_THERMALPNG_PATH, "wb") | |||||
| completed = subprocess.run( | |||||
| *command, | |||||
| cwd=INPUT_DIR, # needed for reative image links | |||||
| stdin=input_file, | |||||
| stdout=output_file | |||||
| ) | |||||
| return completed | |||||
| def make_thermalpreview(): | |||||
| """ | |||||
| exports the preview image | |||||
| """ | |||||
| command = [ | |||||
| '/snap/bin/inkscape', | |||||
| '--pipe', | |||||
| '--export-type=png', | |||||
| '--export-png-color-mode=Gray_8' | |||||
| ], | |||||
| input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb") | |||||
| output_file = open(TEMP_MAP_PREVIEW_PATH, "wb") | |||||
| completed = subprocess.run( | |||||
| *command, | |||||
| cwd=INPUT_DIR, # needed for reative image links | |||||
| stdin=input_file, | |||||
| stdout=output_file | |||||
| ) | |||||
| return completed | |||||
| # def make_thermalpreview(): | |||||
| # """ | |||||
| # exports the preview image | |||||
| # """ | |||||
| # command = [ | |||||
| # '/snap/bin/inkscape', | |||||
| # '--pipe', | |||||
| # '--export-type=png', | |||||
| # '--export-png-color-mode=Gray_8' | |||||
| # ] | |||||
| # input_file = open(TEMP_MAP_THERMALPNG_SVG_PATH, "rb") | |||||
| # output_file = open(TEMP_MAP_PREVIEW_PATH, "wb") | |||||
| # completed = subprocess.run( | |||||
| # *command, | |||||
| # cwd=INPUT_DIR, # needed for reative image links | |||||
| # stdin=input_file, | |||||
| # stdout=output_file | |||||
| # ) | |||||
| # return completed | |||||
| def get_thermal_numpy_array(): | |||||
| # input_file = open(TEMP_MAP_THERMALPNG_PATH, "rb") | |||||
| image = cv2.imread(TEMP_MAP_THERMALPNG_PATH, cv2.IMREAD_ANYDEPTH) | |||||
| image_float = image.astype(np.float32) | |||||
| image_float_normalized = cv2.divide(image_float, 1000) | |||||
| print(image_float_normalized[1000][905]) | |||||
| # cv2.imshow("OpenCV Image Reading", image) | |||||
| return image_float_normalized | |||||
| def get_used_tiles_relpaths(): | |||||
| """ | |||||
| outputs an array of all used tile filenames in the input SVG | |||||
| (relative filepaths like they appear in the svg.) | |||||
| """ | |||||
| images = [] | |||||
| tree = ET.parse(INPUT_PATH) | |||||
| root = tree.getroot() | |||||
| tile_elements = root.xpath('//*[@class="thermal_image"]') | |||||
| linkattrib ='{http://www.w3.org/1999/xlink}href' | |||||
| for tile in tile_elements: | |||||
| images.append(tile.attrib[linkattrib]) | |||||
| return images | |||||
| def deg_coordinates_to_decimal(coordStr): | |||||
| coordArr = coordStr.split(', ') | |||||
| calculatedCoordArray = [] | |||||
| for calculation in coordArr: | |||||
| calculationArr = calculation.split('/') | |||||
| calculatedCoordArray.append(int(calculationArr[0]) / int(calculationArr[1])) | |||||
| degrees = calculatedCoordArray[0] | |||||
| minutes = calculatedCoordArray[1] | |||||
| seconds = calculatedCoordArray[2] | |||||
| decimal = (degrees + (minutes * 1/60) + (seconds * 1/60 * 1/60)) | |||||
| # print(decimal) | |||||
| return decimal | |||||
| def read_coordinates_from_tile(filename): | |||||
| full_filepath = os.path.join(INPUT_DIR, filename) | |||||
| with Image(filename=full_filepath) as image: | |||||
| for key, value in image.metadata.items(): | |||||
| if key == 'exif:GPSLatitude': | |||||
| # print('latstr', value) | |||||
| lat = deg_coordinates_to_decimal(value) # lat -> Y vertical | |||||
| if key == 'exif:GPSLongitude': | |||||
| # print('lonstr', value) | |||||
| lon = deg_coordinates_to_decimal(value) # lon -> X horizontal | |||||
| if key == 'exif:GPSImgDirection': | |||||
| direction = value.split('/') | |||||
| print(int(direction[0])/int(direction[1])/2, ' ', (value)) | |||||
| return [lat, lon] | |||||
| def get_coordinate_boundaries(): | |||||
| image_names = get_used_tiles_relpaths() | |||||
| coordinates = { | |||||
| 'lat': [], | |||||
| 'lon': [] | |||||
| } | |||||
| for filename in image_names: | |||||
| tile_coordinates = read_coordinates_from_tile(filename) | |||||
| coordinates['lat'].append(tile_coordinates[0]) | |||||
| coordinates['lon'].append(tile_coordinates[1]) | |||||
| boundaries = { | |||||
| 'xmin': min(coordinates['lon']), | |||||
| 'xmax': max(coordinates['lon']), | |||||
| 'ymin': min(coordinates['lat']), | |||||
| 'ymax': max(coordinates['lat']), | |||||
| } | |||||
| return boundaries | |||||
| def make_geotiff_image(): | |||||
| thermal_numpy_array = get_thermal_numpy_array() | |||||
| # coordinates of all tiles | |||||
| geo_bound = get_coordinate_boundaries() | |||||
| print('boundaries', geo_bound) | |||||
| np_shape = thermal_numpy_array.shape | |||||
| image_size = (np_shape[0], np_shape[1]) | |||||
| # set geotransform | |||||
| nx = image_size[0] | |||||
| ny = image_size[1] | |||||
| xres = (geo_bound['xmax'] - geo_bound['xmin']) / float(nx) | |||||
| yres = (geo_bound['ymax'] - geo_bound['ymin']) / float(ny) | |||||
| geotransform = (geo_bound['xmin'], xres, 0, geo_bound['ymax'], 0, -yres) | |||||
| # create the 3-band raster file | |||||
| dst_ds = gdal.GetDriverByName('GTiff').Create(OUTPUT_PATH, ny, nx, 1, gdal.GDT_Float32) | |||||
| dst_ds.SetGeoTransform(geotransform) # specify coords | |||||
| srs = osr.SpatialReference() # establish encoding | |||||
| res = srs.SetWellKnownGeogCS( "WGS84" ) # WGS84 lat/long | |||||
| dst_ds.SetProjection(srs.ExportToWkt()) # export coords to file | |||||
| dst_ds.GetRasterBand(1).WriteArray(thermal_numpy_array) # write thermal-band to the raster | |||||
| dst_ds.FlushCache() # write to disk | |||||
| # make_thermalpng_tiles() | |||||
| # make_thermalpng_svg() | |||||
| # make_thermalpreview() | |||||
| # make_thermalpng() | |||||
| make_geotiff_image() | |||||
| # dataset = gdal.Open("working_result_example.tif", gdal.GA_ReadOnly) | |||||
| # print(dir(dataset)) | |||||
| # print(dataset.GetMetadata_List()) | |||||
| @@ -1,23 +1,48 @@ | |||||
| from wand.image import Image | |||||
| import PIL.Image | |||||
| import io | |||||
| import exiftool | |||||
| import subprocess | |||||
| import os | import os | ||||
| import argparse | |||||
| from wand.image import Image | |||||
| import lxml.etree as ET | import lxml.etree as ET | ||||
| import copy | import copy | ||||
| import math | |||||
| from pyproj import CRS | |||||
| from pyproj.aoi import AreaOfInterest | |||||
| from pyproj.database import query_utm_crs_info | |||||
| from pyproj import Transformer | |||||
| arg_parser = argparse.ArgumentParser(description='Place drone FLIR-tiles into a SVG in order to edit them in Inkscape') | |||||
| import cv2 | |||||
| import flirimageextractor | |||||
| from matplotlib import cm | |||||
| import numpy as np | |||||
| import urllib.request | |||||
| arg_parser.add_argument('Input', | |||||
| metavar='input_directory', | |||||
| type=str, | |||||
| help='Path where the FLIR tiles are') | |||||
| arg_parser.add_argument( | |||||
| '--base_rotation', action='store', default=115, | |||||
| help="Base orientation of drone in degrees (0-360) Defaults to 115", | |||||
| type=int, dest='base_rotation' | |||||
| ) | |||||
| arg_parser.add_argument( | |||||
| '--scale', action='store', default=15, | |||||
| help="Scaling (higher number leads to bigger canvas and less dense tiles) (defaults to 15)", | |||||
| type=int, dest='scale' | |||||
| ) | |||||
| args = arg_parser.parse_args() | |||||
| dirname = os.path.dirname(__file__) | dirname = os.path.dirname(__file__) | ||||
| working_dir = 'source_images_full' | |||||
| working_dir = args.Input | |||||
| OUTPUT_PATH = os.path.join(working_dir,'map.svg') | |||||
| filename = os.path.join(dirname, 'canvas.svg') | filename = os.path.join(dirname, 'canvas.svg') | ||||
| @@ -42,25 +67,10 @@ def deg_coordinates_to_decimal(coordStr): | |||||
| seconds = calculatedCoordArray[2] | seconds = calculatedCoordArray[2] | ||||
| return (degrees + (minutes * 1/60) + (seconds * 1/60 * 1/60)) | return (degrees + (minutes * 1/60) + (seconds * 1/60 * 1/60)) | ||||
| # # extracting TIF Data | |||||
| # for root, directories, file in os.walk(os.path.join(dirname, working_dir)): | |||||
| # for file in file: | |||||
| # if(file.endswith(".jpg")): | |||||
| # print(os.path.join(root, file)) | |||||
| # full_filepath = os.path.join(root, file) | |||||
| # with exiftool.ExifTool() as et: | |||||
| # cmd = ['exiftool', full_filepath, "-b", "-RawThermalImage"] | |||||
| # tif_data = subprocess.check_output(cmd) | |||||
| # tif_image = PIL.Image.open(io.BytesIO(tif_data)) | |||||
| # tif_filepath = os.path.join(dirname, working_dir, file.split('.')[0] + '_thermal.tif') | |||||
| # tif_image.save(tif_filepath) | |||||
| # print(tif_filepath) | |||||
| # finding the boundaries of the whole canvas | # finding the boundaries of the whole canvas | ||||
| latsArr = [] | latsArr = [] | ||||
| lonsArr = [] | lonsArr = [] | ||||
| for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | ||||
| for file in file: | for file in file: | ||||
| if(file.endswith(".jpg")): | if(file.endswith(".jpg")): | ||||
| @@ -86,24 +96,71 @@ minLat = min(latsArr) | |||||
| minLon = min(lonsArr) | minLon = min(lonsArr) | ||||
| maxLat = max(latsArr) | maxLat = max(latsArr) | ||||
| maxLon = max(lonsArr) | maxLon = max(lonsArr) | ||||
| width = maxLon - minLon | |||||
| height = maxLat- minLat | |||||
| midLon = (minLon + maxLon) /2 | |||||
| midLat = (minLat + maxLat) /2 | |||||
| # find CRS system | |||||
| utm_crs_list = query_utm_crs_info( | |||||
| datum_name="WGS 84", | |||||
| area_of_interest=AreaOfInterest( | |||||
| west_lon_degree=minLon, | |||||
| south_lat_degree=minLat, | |||||
| east_lon_degree=maxLon, | |||||
| north_lat_degree=maxLat, | |||||
| ), | |||||
| ) | |||||
| utm_crs = CRS.from_epsg(utm_crs_list[0].code) | |||||
| transformer = Transformer.from_crs("EPSG:4326", utm_crs, always_xy=True) | |||||
| min_transformed_lon, min_transformed_lat = transformer.transform(minLon, minLat) | |||||
| max_transformed_lon, max_transformed_lat = transformer.transform(maxLon, maxLat) | |||||
| width = max_transformed_lon - min_transformed_lon | |||||
| height = max_transformed_lat - min_transformed_lat | |||||
| # def latlngToGlobalXY(lat, lng): | |||||
| # earth_radius = 6371 | |||||
| # # Calculates x based on cos of average of the latitudes | |||||
| # x = earth_radius * lng * math.cos((minLat + maxLat)/2) | |||||
| # # Calculates y based on latitude | |||||
| # y = earth_radius * lat | |||||
| # return {x: x, y: y} | |||||
| # def latlngToScreenXY(lat, lng): | |||||
| # topLeft_corner = latlngToGlobalXY(minLat, minLon) | |||||
| # bottomRight_corner = latlngToGlobalXY(maxLat, maxLon) | |||||
| # # Calculate global X and Y for projection point | |||||
| # pos = latlngToGlobalXY(lat, lng) | |||||
| # # Calculate the percentage of Global X position in relation to total global width | |||||
| # pos.perX = ((pos.x - topLeft_corner.x) / (bottomRight_corner.x - topLeft_corner.x)) | |||||
| # # Calculate the percentage of Global Y position in relation to total global height | |||||
| # pos.perY = ((pos.y - topLeft_corner.y) / (bottomRight_corner.y - topLeft_corner.y)) | |||||
| # # Returns the screen position based on reference points | |||||
| # return { | |||||
| # x: p0.scrX + (p1.scrX - p0.scrX)*pos.perX, | |||||
| # y: p0.scrY + (p1.scrY - p0.scrY)*pos.perY | |||||
| # } | |||||
| # placing the images into the svg | # placing the images into the svg | ||||
| rotation = 125 | |||||
| y_scale = -1800000 #-400000 | |||||
| x_scale = 655000 #-950000 | |||||
| # y_scale = 2600000 #-400000 | |||||
| # x_scale = 1200000 #-950000 | |||||
| image_rotation_up = rotation #32 | |||||
| image_rotation_down = rotation + 180 #192 | |||||
| # image_rotation_up = rotation #32 | |||||
| # image_rotation_down = rotation + 180 #192 | |||||
| for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | ||||
| for file in file: | for file in file: | ||||
| @@ -111,52 +168,48 @@ for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | |||||
| print(os.path.join(root_path, file)) | print(os.path.join(root_path, file)) | ||||
| full_filepath = os.path.join(root_path, file) | full_filepath = os.path.join(root_path, file) | ||||
| with Image(filename=full_filepath) as image: | with Image(filename=full_filepath) as image: | ||||
| print(image.width) | |||||
| print(image.height) | |||||
| # print(image.width) | |||||
| # print(image.height) | |||||
| for key, value in image.metadata.items(): | for key, value in image.metadata.items(): | ||||
| # print("{}: {}".format(key, value)) | # print("{}: {}".format(key, value)) | ||||
| if key == 'exif:GPSLatitude': | if key == 'exif:GPSLatitude': | ||||
| lat = deg_coordinates_to_decimal(value) - minLat | |||||
| print('lat '+ str(lat)) | |||||
| lat = deg_coordinates_to_decimal(value) | |||||
| lat_offset = lat - minLat | |||||
| if key == 'exif:GPSLongitude': | if key == 'exif:GPSLongitude': | ||||
| lon = deg_coordinates_to_decimal(value) - minLon | |||||
| print('lon '+ str(lon)) | |||||
| lon = deg_coordinates_to_decimal(value) | |||||
| lon_offset = lon - minLon | |||||
| if key == 'exif:GPSImgDirection': | if key == 'exif:GPSImgDirection': | ||||
| direction = value.split('/') | direction = value.split('/') | ||||
| rotation = int(direction[0])/int(direction[1])/2 | |||||
| rotation = ( int(direction[0]) / int(direction[1]) ) / 2 + args.base_rotation | |||||
| print('rotation',rotation) | |||||
| transformed_lon, transformed_lat = transformer.transform(lon, lat) | |||||
| lon_offset = transformed_lon - min_transformed_lon | |||||
| lat_offset = transformed_lat - min_transformed_lat | |||||
| # print(transformed_lon, min_transformed_lon, transformed_lat, min_transformed_lat) | |||||
| # print('lon_offset, lat_offset', lon_offset, lat_offset) | |||||
| g_pos_el_attributes = { | g_pos_el_attributes = { | ||||
| # 'x': str(lat*scale), | |||||
| # 'y': str(lon*scale), | |||||
| 'transform': "translate({}, {})".format(format(lon*x_scale, '.20f'), format(lat*y_scale, '.20f')), | |||||
| 'data-lat': format(lat, '.20f'), | |||||
| 'data-lon': format(lon, '.20f'), | |||||
| 'transform': "translate({}, {})".format(format(lon_offset*args.scale, '.20f'), format(lat_offset*args.scale*-1, '.20f')), | |||||
| 'data-lat-offset': format(lat_offset, '.20f'), | |||||
| 'data-lon-offset': format(lon_offset, '.20f'), | |||||
| 'class': 'tile', | 'class': 'tile', | ||||
| 'id': 'tile_{}'.format(file.split('.')[0]), | 'id': 'tile_{}'.format(file.split('.')[0]), | ||||
| # 'style': 'opacity:.6', | |||||
| } | } | ||||
| g_pos_el = ET.SubElement(main_layer, 'g', attrib=g_pos_el_attributes) | g_pos_el = ET.SubElement(main_layer, 'g', attrib=g_pos_el_attributes) | ||||
| g_offset_corr_el_attributes = { | g_offset_corr_el_attributes = { | ||||
| 'transform': "translate(150, 0)", | |||||
| 'transform': "translate({}, {})".format(-image.width/2, -image.height/2), | |||||
| 'class': 'tile-offset-corr', | 'class': 'tile-offset-corr', | ||||
| } | } | ||||
| g_offset_corr_el = ET.SubElement(g_pos_el, 'g', attrib=g_offset_corr_el_attributes) | g_offset_corr_el = ET.SubElement(g_pos_el, 'g', attrib=g_offset_corr_el_attributes) | ||||
| g_center_el_attributes = { | |||||
| 'class': 'tile-center', | |||||
| 'transform': 'translate({}, {})'.format(str(image.width/2*-1), str(image.height/2*-1)) | |||||
| } | |||||
| g_center_el = ET.SubElement(g_offset_corr_el, 'g', attrib=g_center_el_attributes) | |||||
| g_rot_el_attributes = { | g_rot_el_attributes = { | ||||
| 'class': 'tile-rotate', | 'class': 'tile-rotate', | ||||
| 'data-image-rotation': str(image_rotation_up), | |||||
| 'data-image-dimensions': str(image.width/2) + ' ' + str(image.height/2), | |||||
| 'transform': 'rotate({} {} {})'.format(str(image_rotation_up), str(image.width/2), str(image.height/2)) | |||||
| 'data-image-rotation': str(rotation), | |||||
| 'data-image-dimensions': str(image.width) + ' ' + str(image.height), | |||||
| 'transform': 'rotate({} {} {})'.format(str(rotation), str(image.width/2), str(image.height/2)) | |||||
| # 'transform': 'rotate({} {} {})'.format(str(rotation), 0,0) | |||||
| } | } | ||||
| g_rot_el = ET.SubElement(g_center_el, 'g', attrib=g_rot_el_attributes) | |||||
| g_rot_el = ET.SubElement(g_offset_corr_el, 'g', attrib=g_rot_el_attributes) | |||||
| xlinkns ="http://www.w3.org/1999/xlink" | xlinkns ="http://www.w3.org/1999/xlink" | ||||
| image_el = ET.SubElement(g_rot_el, 'image', { | image_el = ET.SubElement(g_rot_el, 'image', { | ||||
| @@ -164,14 +217,10 @@ for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | |||||
| "{%s}href" % xlinkns: file, | "{%s}href" % xlinkns: file, | ||||
| "width": str(image.width), | "width": str(image.width), | ||||
| "height": str(image.height), | "height": str(image.height), | ||||
| "mask" : "url(#tilefademask)", | |||||
| 'data-lat': format(lat, '.20f'), | |||||
| 'data-lon': format(lon, '.20f'), | |||||
| }) | }) | ||||
| # transform_str = "translate(-{}, -{})".format(str(min(latsArr)*scale), str(min(lonsArr)*scale)) | |||||
| # print(transform_str) | |||||
| # main_layer.attrib['transform'] = transform_str | |||||
| # sort elements | # sort elements | ||||
| def getkey(elem): | def getkey(elem): | ||||
| @@ -184,55 +233,62 @@ def getkey(elem): | |||||
| main_layer[:] = sorted(main_layer, key=getkey) | main_layer[:] = sorted(main_layer, key=getkey) | ||||
| # rotate image if previous element is under the current one | |||||
| # find rows | |||||
| # up/down is actually left/right or right/left | |||||
| last_state = 'down' | last_state = 'down' | ||||
| for index, el in enumerate(main_layer): | for index, el in enumerate(main_layer): | ||||
| if(el.getprevious() is not None): | if(el.getprevious() is not None): | ||||
| if (el.getprevious().attrib['data-lon'] > el.attrib['data-lon'] or (el.getprevious().attrib['data-lon'] == el.attrib['data-lon'] and last_state == 'up')): | |||||
| if (el.getprevious().attrib['data-lon-offset'] > el.attrib['data-lon-offset'] or (el.getprevious().attrib['data-lon-offset'] == el.attrib['data-lon-offset'] and last_state == 'up')): | |||||
| print('up') | print('up') | ||||
| rot_el = el[0][0][0] | |||||
| # print(rot_el.attrib['data-image-rotation']) | |||||
| # print(rot_el.attrib['data-image-dimensions']) | |||||
| rot_el = el[0][0] | |||||
| el.attrib['data-direction'] = 'up' | el.attrib['data-direction'] = 'up' | ||||
| # print(el.attrib['data-lat'], el.getprevious().attrib['data-lat']) | |||||
| else: | else: | ||||
| rot_el = el[0][0][0] | |||||
| rot_el = el[0][0] | |||||
| el.attrib['data-direction'] = 'down' | el.attrib['data-direction'] = 'down' | ||||
| # el.attrib['style'] = 'opacity:0' | |||||
| new_rotation = image_rotation_down #float(rot_el.attrib['data-image-rotation']) + 180 | |||||
| rot_el.attrib['transform'] = "rotate({} {})".format(str(new_rotation), rot_el.attrib['data-image-dimensions']) | |||||
| print('down') | print('down') | ||||
| # print(rot_el.attrib['data-image-rotation']) | |||||
| # print(rot_el.attrib['data-image-dimensions']) | |||||
| # NOT NEEDED SINCE THERE IS A ROTATION INFORMATION | |||||
| # merge tiles into groups | # merge tiles into groups | ||||
| print(index) | |||||
| print("el.attrib['data-direction'] " + el.attrib['data-direction']) | |||||
| print("last_state " + last_state) | |||||
| if index is 1 or last_state != el.attrib['data-direction']: | |||||
| # print(index) | |||||
| # print("el.attrib['data-direction'] " + el.attrib['data-direction']) | |||||
| # print("last_state " + last_state) | |||||
| if index is 1 or last_state is not el.attrib['data-direction']: | |||||
| current_row = ET.SubElement(tile_rows, 'g', attrib={ 'class': 'tile-row' }) | current_row = ET.SubElement(tile_rows, 'g', attrib={ 'class': 'tile-row' }) | ||||
| copyElem = copy.deepcopy(el) | copyElem = copy.deepcopy(el) | ||||
| current_row.insert(0, copyElem) | current_row.insert(0, copyElem) | ||||
| last_state = el.attrib['data-direction'] | last_state = el.attrib['data-direction'] | ||||
| # remove temporary group | |||||
| root.remove(main_layer) | root.remove(main_layer) | ||||
| # resize canvas to tiles and add some padding | |||||
| with open(os.path.join(working_dir,'map.svg'), 'wb') as f: | |||||
| tree.write(f, encoding='utf-8') | |||||
| print(width, height, args.scale) | |||||
| scaled_width = width * args.scale | |||||
| scaled_height = height * args.scale | |||||
| padding = 500 | |||||
| canvas_width = str(scaled_width + padding*2) | |||||
| canvas_height = str(scaled_height + padding*2) | |||||
| viewbox_x = str(padding * -1) | |||||
| viewbox_y = str((scaled_height + padding) * -1) | |||||
| viewbox_width = canvas_width | |||||
| viewbox_height = canvas_height | |||||
| # # get some base satellite map for reference | |||||
| # apikey = "MYaMHCLtPz1fUfe0FzZqOMI35m893jIV80oeHG19Piw" | |||||
| # lon_center = | |||||
| # lat_center = | |||||
| # zoom = | |||||
| # map_width = | |||||
| # request = "https://image.maps.ls.hereapi.com/mia/1.6/mapview?apiKey={}&c={},{}&sb=mk&t=1&z={}&w={}&nodot".format(apikey, lon_center, lat_center, zoom, map_width) | |||||
| root.attrib['width'] = canvas_width | |||||
| root.attrib['height'] = canvas_height | |||||
| root.attrib['viewBox'] = "{} {} {} {}".format(viewbox_x, viewbox_y, viewbox_width, viewbox_height) | |||||
| # Finally save the svg | |||||
| with open(OUTPUT_PATH, 'wb') as f: | |||||
| tree.write(f, encoding='utf-8') | |||||
| # svg = ET.tostring(tree, encoding="unicode") | |||||
| # print(svg) | |||||
| print('Done!') | print('Done!') | ||||