@@ -19,9 +19,6 @@ | |||
xmlns:cc="http://creativecommons.org/ns#" | |||
xmlns:dc="http://purl.org/dc/elements/1.1/"> | |||
<style> | |||
.thermal_image { | |||
opacity: .5 ; | |||
} | |||
g.tile[data-direction='down'] { | |||
opacity: 1; | |||
} | |||
@@ -30,7 +27,7 @@ g.tile[data-direction='down'] { | |||
} --> | |||
</style> | |||
<defs id="defs2"> | |||
<!-- <filter | |||
<filter | |||
xmlns="http://www.w3.org/2000/svg" | |||
style="color-interpolation-filters:sRGB;" | |||
inkscape:label="Blur" | |||
@@ -56,7 +53,7 @@ g.tile[data-direction='down'] { | |||
y="-25" | |||
style="fill:#ffffff;filter:url(#filter156128)" | |||
transform="matrix(0.77703373,0,0,0.74018882,207.24036,100.70559)" /> | |||
</mask> --> | |||
</mask> | |||
</defs> | |||
<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 argparse | |||
from wand.image import Image | |||
import lxml.etree as ET | |||
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__) | |||
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') | |||
@@ -42,25 +67,10 @@ def deg_coordinates_to_decimal(coordStr): | |||
seconds = calculatedCoordArray[2] | |||
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 | |||
latsArr = [] | |||
lonsArr = [] | |||
for root_path, directories, file in os.walk(os.path.join(dirname, working_dir)): | |||
for file in file: | |||
if(file.endswith(".jpg")): | |||
@@ -86,24 +96,71 @@ minLat = min(latsArr) | |||
minLon = min(lonsArr) | |||
maxLat = max(latsArr) | |||
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 | |||
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 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)) | |||
full_filepath = os.path.join(root_path, file) | |||
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(): | |||
# print("{}: {}".format(key, value)) | |||
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': | |||
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': | |||
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 = { | |||
# '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', | |||
'id': 'tile_{}'.format(file.split('.')[0]), | |||
# 'style': 'opacity:.6', | |||
} | |||
g_pos_el = ET.SubElement(main_layer, 'g', attrib=g_pos_el_attributes) | |||
g_offset_corr_el_attributes = { | |||
'transform': "translate(150, 0)", | |||
'transform': "translate({}, {})".format(-image.width/2, -image.height/2), | |||
'class': 'tile-offset-corr', | |||
} | |||
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 = { | |||
'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" | |||
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, | |||
"width": str(image.width), | |||
"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 | |||
def getkey(elem): | |||
@@ -184,55 +233,62 @@ def getkey(elem): | |||
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' | |||
for index, el in enumerate(main_layer): | |||
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') | |||
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' | |||
# print(el.attrib['data-lat'], el.getprevious().attrib['data-lat']) | |||
else: | |||
rot_el = el[0][0][0] | |||
rot_el = el[0][0] | |||
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(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 | |||
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' }) | |||
copyElem = copy.deepcopy(el) | |||
current_row.insert(0, copyElem) | |||
last_state = el.attrib['data-direction'] | |||
# remove temporary group | |||
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!') | |||