diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c14ad4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,204 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,executable +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,executable + +### Executable ### +*.app +*.bat +*.cgi +*.com +*.exe +*.gadget +*.jar +*.pif +*.vb +*.wsf + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,executable diff --git a/README.md b/README.md index e995bfb..bc15bbe 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,12 @@ This small Python tool/library allows to extract the original photo and thermal ## Requirements -This tool relies on `exiftool`. It should be available in most Linux distributions (e.g. as `perl-image-exiftool` in Arch Linux or `libimage-exiftool-perl` in Debian and Ubuntu). +This tool relies on `exiftool`. It should be available in most Linux distributions (e.g. as `perl-image-exiftool` in Arch Linux or `libimage-exiftool-perl` in Debian and Ubuntu). On Windows, download the portable version and use its path as the `-exif` argument. -It also needs the Python packages *numpy* and *matplotlib* (the latter only if used interactively). +In order to install all the dependencies for this repository, use the `requirements.txt` file: ```bash -# sudo apt update -# sudo apt install exiftool -# sudo pip install numpy matplotlib +pip install -r requirements.txt ``` ## Usage @@ -38,7 +36,7 @@ python flir_image_extractor.py -p -i 'examples/zenmuse_xtr.jpg' ```bash usage: flir_image_extractor.py [-h] -i INPUT [-p] [-exif EXIFTOOL] - [-csv EXTRACTCSV] [-d] + [-csv EXTRACTCSV] [-xlsx EXTRACTXLSX] [-d] Extract and visualize Flir Image data @@ -50,7 +48,9 @@ optional arguments: -exif EXIFTOOL, --exiftool EXIFTOOL Custom path to exiftool -csv EXTRACTCSV, --extractcsv EXTRACTCSV - Export the thermal data per pixel encoded as csv file + Export the thermal data matrix encoded as csv file + -xlsx EXTRACTXLSX, --extractxlsx EXTRACTXLSX + Export the thermal data per pixel encoded as xlsx file -d, --debug Set the debug flag ``` diff --git a/flir_image_extractor.py b/flir_image_extractor.py index b821dc2..d4b7b6e 100644 --- a/flir_image_extractor.py +++ b/flir_image_extractor.py @@ -9,7 +9,6 @@ import os import os.path import re -import csv import subprocess from PIL import Image from math import sqrt, exp, log @@ -17,6 +16,7 @@ from matplotlib import pyplot as plt import numpy as np +import pandas as pd class FlirImageExtractor: @@ -33,11 +33,11 @@ def __init__(self, exiftool_path="exiftool", is_debug=False): # valid for PNG thermal images self.use_thumbnail = False self.fix_endian = True + self.rgb_available = True self.rgb_image_np = None self.thermal_image_np = None - pass def process_image(self, flir_img_filename): """ @@ -47,7 +47,7 @@ def process_image(self, flir_img_filename): :return: """ if self.is_debug: - print("INFO Flir image filepath:{}".format(flir_img_filename)) + print("INFO Flir image filepath: {}".format(flir_img_filename)) if not os.path.isfile(flir_img_filename): raise ValueError("Input file does not exist or this user don't have permission on this file") @@ -59,7 +59,11 @@ def process_image(self, flir_img_filename): self.use_thumbnail = True self.fix_endian = False - self.rgb_image_np = self.extract_embedded_image() + try: + self.rgb_image_np = self.extract_embedded_image() + except: + self.rgb_available = False + print("Embedded image not available") self.thermal_image_np = self.extract_thermal_image() def get_image_type(self): @@ -208,57 +212,66 @@ def plot(self): Plot the rgb + thermal image (easy to see the pixel values) :return: """ - rgb_np = self.get_rgb_np() thermal_np = self.get_thermal_np() - - plt.subplot(1, 2, 1) - plt.imshow(thermal_np, cmap='hot') - plt.subplot(1, 2, 2) - plt.imshow(rgb_np) - plt.show() + if self.rgb_available: + rgb_np = self.get_rgb_np() + + plt.subplot(1, 2, 1) + plt.imshow(thermal_np, cmap='turbo') + plt.subplot(1, 2, 2) + plt.imshow(rgb_np) + plt.show() + else: + plt.imshow(thermal_np, cmap='turbo') + plt.show() def save_images(self): """ Save the extracted images :return: """ - rgb_np = self.get_rgb_np() - thermal_np = self.extract_thermal_image() + fn_prefix, _ = os.path.splitext(self.flir_img_filename) - img_visual = Image.fromarray(rgb_np) - thermal_normalized = (thermal_np - np.amin(thermal_np)) / (np.amax(thermal_np) - np.amin(thermal_np)) - img_thermal = Image.fromarray(np.uint8(cm.inferno(thermal_normalized) * 255)) + if self.rgb_available: + rgb_np = self.get_rgb_np() + img_visual = Image.fromarray(rgb_np) + image_filename = fn_prefix + self.image_suffix + if self.use_thumbnail: + image_filename = fn_prefix + self.thumbnail_suffix + if self.is_debug: + print("DEBUG Saving RGB image to: {}".format(image_filename)) + img_visual.save(image_filename) - fn_prefix, _ = os.path.splitext(self.flir_img_filename) + thermal_np = self.extract_thermal_image() + thermal_normalized = (thermal_np - np.amin(thermal_np)) / (np.amax(thermal_np) - np.amin(thermal_np)) + img_thermal = Image.fromarray(np.uint8(cm.turbo(thermal_normalized) * 255)) thermal_filename = fn_prefix + self.thermal_suffix - image_filename = fn_prefix + self.image_suffix - if self.use_thumbnail: - image_filename = fn_prefix + self.thumbnail_suffix - if self.is_debug: - print("DEBUG Saving RGB image to:{}".format(image_filename)) - print("DEBUG Saving Thermal image to:{}".format(thermal_filename)) - - img_visual.save(image_filename) + print("DEBUG Saving Thermal image to: {}".format(thermal_filename)) img_thermal.save(thermal_filename) def export_thermal_to_csv(self, csv_filename): """ - Convert thermal data in numpy to json + Convert thermal data in numpy to csv :return: """ + if self.is_debug: + print("DEBUG Saving csv to: {}".format(csv_filename)) + np.savetxt(csv_filename, self.thermal_image_np, delimiter=',', fmt='%1.2f') - with open(csv_filename, 'w') as fh: - writer = csv.writer(fh, delimiter=',') - writer.writerow(['x', 'y', 'temp (c)']) - - pixel_values = [] - for e in np.ndenumerate(self.thermal_image_np): - x, y = e[0] - c = e[1] - pixel_values.append([x, y, c]) - - writer.writerows(pixel_values) + def export_thermal_to_xlsx(self, xlsx_filename): + """ + Convert thermal data in numpy to xlsx + :return: + """ + data = [] + for coordinates, temp in np.ndenumerate(self.thermal_image_np): + x, y = coordinates + data.append([x, y, temp]) + df = pd.DataFrame(data, columns=["x", "y", "temp_celsius"]) + if self.is_debug: + print("DEBUG Saving xlsx to: {}".format(xlsx_filename)) + df.to_excel(xlsx_filename, index=False) if __name__ == '__main__': @@ -267,7 +280,9 @@ def export_thermal_to_csv(self, csv_filename): parser.add_argument('-p', '--plot', help='Generate a plot using matplotlib', required=False, action='store_true') parser.add_argument('-exif', '--exiftool', type=str, help='Custom path to exiftool', required=False, default='exiftool') - parser.add_argument('-csv', '--extractcsv', help='Export the thermal data per pixel encoded as csv file', + parser.add_argument('-csv', '--extractcsv', help='Export the thermal data matrix encoded as csv file', + required=False) + parser.add_argument('-xlsx', '--extractxlsx', help='Export the thermal data per pixel encoded as xlsx file', required=False) parser.add_argument('-d', '--debug', help='Set the debug flag', required=False, action='store_true') @@ -281,5 +296,8 @@ def export_thermal_to_csv(self, csv_filename): if args.extractcsv: fie.export_thermal_to_csv(args.extractcsv) + + if args.extractxlsx: + fie.export_thermal_to_xlsx(args.extractxlsx) fie.save_images() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..20a7341 Binary files /dev/null and b/requirements.txt differ