From 8d6e8be8f46d13599947a9bbaa4951bc90838782 Mon Sep 17 00:00:00 2001 From: Xerbo Date: Mon, 16 Jan 2023 22:25:43 +0000 Subject: [PATCH] Version 2.0.0 work --- .gitignore | 51 ++- .gitmodules | 4 +- CMakeLists.txt | 130 +++---- CONTRIBUTING.md | 31 +- README.md | 123 ++++--- aptdec-cli/main.c | 490 +++++++++++++++++++++++++ aptdec-cli/pngio.c | 153 ++++++++ aptdec-cli/pngio.h | 41 +++ {src => aptdec-cli}/util.c | 11 +- {src => aptdec-cli}/util.h | 22 +- build_windows.bat | 6 +- build_windows.sh | 3 +- cmake/FindLibSndFile.cmake | 19 - {src => libaptdec}/algebra.c | 68 +++- {src => libaptdec}/algebra.h | 17 +- libaptdec/calibration.c | 121 ++++++ {src => libaptdec}/calibration.h | 10 +- libaptdec/color.c | 94 +++++ libaptdec/dsp.c | 237 ++++++++++++ libaptdec/effects.c | 175 +++++++++ libaptdec/filter.c | 109 ++++++ {src => libaptdec}/filter.h | 16 +- libaptdec/image.c | 236 ++++++++++++ libaptdec/include/aptdec.h | 169 +++++++++ libaptdec/util.c | 40 ++ libaptdec/util.h | 29 ++ luts/N19-HRPT-Falsecolor.png | Bin 0 -> 16186 bytes luts/WXtoImg-BD.png | Bin 0 -> 1613 bytes luts/WXtoImg-CC.png | Bin 0 -> 1603 bytes luts/WXtoImg-EC.png | Bin 0 -> 1632 bytes luts/WXtoImg-HE.png | Bin 0 -> 1736 bytes luts/WXtoImg-HF.png | Bin 0 -> 1683 bytes luts/WXtoImg-JF.png | Bin 0 -> 1637 bytes luts/WXtoImg-JJ.png | Bin 0 -> 1801 bytes luts/WXtoImg-MB.png | Bin 0 -> 1604 bytes luts/WXtoImg-MD.png | Bin 0 -> 1683 bytes {palettes => luts}/WXtoImg-N15-HVC.png | Bin 15665 -> 12297 bytes {palettes => luts}/WXtoImg-N18-HVC.png | Bin 15061 -> 11693 bytes {palettes => luts}/WXtoImg-N19-HVC.png | Bin 15528 -> 12160 bytes luts/WXtoImg-NO.png | Bin 0 -> 2062 bytes luts/WXtoImg-TA.png | Bin 0 -> 1553 bytes luts/WXtoImg-ZA.png | Bin 0 -> 1554 bytes {palettes => luts}/WXtoImg-class.png | Bin 13752 -> 10384 bytes luts/WXtoImg-fire.png | Bin 0 -> 1565 bytes luts/WXtoImg-sea.png | Bin 0 -> 1574 bytes palettes/N19-HRPT-Falsecolor.png | Bin 15849 -> 0 bytes palettes/WXtoImg-BD.png | Bin 4981 -> 0 bytes palettes/WXtoImg-CC.png | Bin 4971 -> 0 bytes palettes/WXtoImg-EC.png | Bin 5000 -> 0 bytes palettes/WXtoImg-HE.png | Bin 5104 -> 0 bytes palettes/WXtoImg-HF.png | Bin 5051 -> 0 bytes palettes/WXtoImg-JF.png | Bin 5005 -> 0 bytes palettes/WXtoImg-JJ.png | Bin 5169 -> 0 bytes palettes/WXtoImg-MB.png | Bin 4972 -> 0 bytes palettes/WXtoImg-MD.png | Bin 5051 -> 0 bytes palettes/WXtoImg-NO.png | Bin 5430 -> 0 bytes palettes/WXtoImg-TA.png | Bin 4921 -> 0 bytes palettes/WXtoImg-ZA.png | Bin 4922 -> 0 bytes palettes/WXtoImg-fire.png | Bin 4933 -> 0 bytes palettes/WXtoImg-sea.png | Bin 4942 -> 0 bytes src/apt.h | 126 ------- src/argparse | 1 - src/calibration.c | 101 ----- src/color.c | 83 ----- src/color.h | 3 - src/common.h | 60 --- src/dsp.c | 258 ------------- src/filter.c | 62 ---- src/image.c | 388 -------------------- src/image.h | 2 - src/libs/median.c | 56 --- src/main.c | 316 ---------------- src/pngio.c | 411 --------------------- src/pngio.h | 11 - src/taps.h | 80 ---- util/PrecipitationPalette.png | Bin 1478 -> 199 bytes util/TempPalette.png | Bin 901 -> 454 bytes util/Temperature.png | Bin 2014 -> 1596 bytes util/img2gradient.py | 37 ++ util/img2pal.py | 26 -- 80 files changed, 2240 insertions(+), 2186 deletions(-) create mode 100644 aptdec-cli/main.c create mode 100644 aptdec-cli/pngio.c create mode 100644 aptdec-cli/pngio.h rename {src => aptdec-cli}/util.c (83%) rename {src => aptdec-cli}/util.h (68%) delete mode 100644 cmake/FindLibSndFile.cmake rename {src => libaptdec}/algebra.c (56%) rename {src => libaptdec}/algebra.h (71%) create mode 100644 libaptdec/calibration.c rename {src => libaptdec}/calibration.h (87%) create mode 100644 libaptdec/color.c create mode 100644 libaptdec/dsp.c create mode 100644 libaptdec/effects.c create mode 100644 libaptdec/filter.c rename {src => libaptdec}/filter.h (66%) create mode 100644 libaptdec/image.c create mode 100644 libaptdec/include/aptdec.h create mode 100644 libaptdec/util.c create mode 100644 libaptdec/util.h create mode 100644 luts/N19-HRPT-Falsecolor.png create mode 100644 luts/WXtoImg-BD.png create mode 100644 luts/WXtoImg-CC.png create mode 100644 luts/WXtoImg-EC.png create mode 100644 luts/WXtoImg-HE.png create mode 100644 luts/WXtoImg-HF.png create mode 100644 luts/WXtoImg-JF.png create mode 100644 luts/WXtoImg-JJ.png create mode 100644 luts/WXtoImg-MB.png create mode 100644 luts/WXtoImg-MD.png rename {palettes => luts}/WXtoImg-N15-HVC.png (70%) rename {palettes => luts}/WXtoImg-N18-HVC.png (70%) rename {palettes => luts}/WXtoImg-N19-HVC.png (71%) create mode 100644 luts/WXtoImg-NO.png create mode 100644 luts/WXtoImg-TA.png create mode 100644 luts/WXtoImg-ZA.png rename {palettes => luts}/WXtoImg-class.png (63%) create mode 100644 luts/WXtoImg-fire.png create mode 100644 luts/WXtoImg-sea.png delete mode 100644 palettes/N19-HRPT-Falsecolor.png delete mode 100644 palettes/WXtoImg-BD.png delete mode 100644 palettes/WXtoImg-CC.png delete mode 100644 palettes/WXtoImg-EC.png delete mode 100644 palettes/WXtoImg-HE.png delete mode 100644 palettes/WXtoImg-HF.png delete mode 100644 palettes/WXtoImg-JF.png delete mode 100644 palettes/WXtoImg-JJ.png delete mode 100644 palettes/WXtoImg-MB.png delete mode 100644 palettes/WXtoImg-MD.png delete mode 100644 palettes/WXtoImg-NO.png delete mode 100644 palettes/WXtoImg-TA.png delete mode 100644 palettes/WXtoImg-ZA.png delete mode 100644 palettes/WXtoImg-fire.png delete mode 100644 palettes/WXtoImg-sea.png delete mode 100644 src/apt.h delete mode 160000 src/argparse delete mode 100644 src/calibration.c delete mode 100644 src/color.c delete mode 100644 src/color.h delete mode 100644 src/common.h delete mode 100644 src/dsp.c delete mode 100644 src/filter.c delete mode 100644 src/image.c delete mode 100644 src/image.h delete mode 100644 src/libs/median.c delete mode 100644 src/main.c delete mode 100644 src/pngio.c delete mode 100644 src/pngio.h delete mode 100644 src/taps.h create mode 100755 util/img2gradient.py delete mode 100644 util/img2pal.py diff --git a/.gitignore b/.gitignore index 85d013c..69c083f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -# Created by https://www.gitignore.io/api/c -# Edit at https://www.gitignore.io/?templates=c +# Created by https://www.toptal.com/developers/gitignore/api/c,cmake +# Edit at https://www.toptal.com/developers/gitignore?templates=c,cmake + +### C ### +# Prerequisites +*.d # Object files *.o @@ -26,6 +30,7 @@ *.dll *.so *.so.* +*.dylib # Executables *.exe @@ -33,6 +38,7 @@ *.app *.i*86 *.x86_64 +*.hex # Debug files *.dSYM/ @@ -40,19 +46,40 @@ *.idb *.pdb -# Program specifics +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +# External projects +*-prefix/ + +# End of https://www.toptal.com/developers/gitignore/api/c,cmake + *.png !textlogo.png -!palettes/*.png +!luts/*.png !util/*.png + *.wav -aptdec +*.ogg -.vscode/ build/ -libpng/ -zlib/ -winbuild/ -winpath/ -libsndfile-1.0.29-win64.zip -libsndfile-1.0.29-win64/ diff --git a/.gitmodules b/.gitmodules index f1e9861..619fa46 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "src/argparse"] - path = src/argparse +[submodule "aptdec-cli/argparse"] + path = aptdec-cli/argparse url = https://github.com/cofyc/argparse diff --git a/CMakeLists.txt b/CMakeLists.txt index adf190c..843a97d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,81 +1,86 @@ -cmake_minimum_required (VERSION 3.0.0) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") +cmake_minimum_required(VERSION 3.0.0) -project(aptdec C) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_EXTENSIONS OFF) + +# Get version +find_package(Git) +if (GIT_FOUND) + execute_process(COMMAND ${GIT_EXECUTABLE} describe --tag WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} OUTPUT_VARIABLE GIT_TAG OUTPUT_STRIP_TRAILING_WHITESPACE) + set(VERSION "${GIT_TAG}") +else() + set(VERSION "Unknown") +endif() +project(aptdec C) include(GNUInstallDirs) -# libpng +# aptdec-cli find_package(PNG) - -# libsndfile -find_package(LibSndFile) - -set(LIB_C_SOURCE_FILES src/color.c src/dsp.c src/filter.c src/image.c src/algebra.c src/libs/median.c src/util.c src/calibration.c) -set(EXE_C_SOURCE_FILES src/main.c src/pngio.c src/argparse/argparse.c src/util.c) -set(LIB_C_HEADER_FILES src/apt.h) - -# Link with static library for aptdec executable, so we don't need to set the path -add_library(aptstatic STATIC ${LIB_C_SOURCE_FILES}) - -# Create shared library for 3rd party apps -add_library(apt SHARED ${LIB_C_SOURCE_FILES}) -set_target_properties(apt PROPERTIES PUBLIC_HEADER ${LIB_C_HEADER_FILES}) - -add_compile_definitions(PALETTE_DIR="${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}/palettes") - -if (PNG_FOUND AND LIBSNDFILE_FOUND) - add_executable(aptdec ${EXE_C_SOURCE_FILES}) - include_directories(${PNG_PNG_INCLUDE_DIR}) - include_directories(${LIBSNDFILE_INCLUDE_DIR}) - target_link_libraries(aptdec PRIVATE PNG::PNG) - target_link_libraries(aptdec PRIVATE ${LIBSNDFILE_LIBRARY}) - target_link_libraries(aptdec PRIVATE aptstatic) - if (MSVC) - target_compile_options(aptdec PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPT_API_STATIC) +find_path(SNDFILE_INCLUDE_DIR sndfile.h) +find_library(SNDFILE_LIBRARIES NAMES sndfile libsndfile PATH) + +if(PNG_FOUND AND SNDFILE_LIBRARIES AND SNDFILE_INCLUDE_DIR) + set(APTDEC_CLI_SOURCE_FILES + aptdec-cli/argparse/argparse.c + aptdec-cli/main.c + aptdec-cli/pngio.c + aptdec-cli/util.c + ) + add_executable(aptdec-cli ${APTDEC_CLI_SOURCE_FILES}) + + target_compile_definitions(aptdec-cli PRIVATE "VERSION=\"${VERSION}\"") + target_include_directories(aptdec-cli PRIVATE ${PNG_INCLUDE_DIRS}) + target_include_directories(aptdec-cli PRIVATE ${SNDFILE_INCLUDE_DIR}) + target_include_directories(aptdec-cli PRIVATE libaptdec/include/) + target_link_libraries(aptdec-cli PRIVATE ${PNG_LIBRARIES}) + target_link_libraries(aptdec-cli PRIVATE ${SNDFILE_LIBRARIES}) + target_link_libraries(aptdec-cli PRIVATE aptdec) + + if(MSVC) + target_compile_options(aptdec-cli PRIVATE /D_CRT_SECURE_NO_WARNINGS=1) else() - # Math - target_link_libraries(aptdec PRIVATE m) - - # Throw errors on warnings on release builds - if(CMAKE_BUILD_TYPE MATCHES "Release") - target_compile_options(aptdec PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) - else() - target_compile_options(aptdec PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) - endif() + target_link_libraries(aptdec-cli PRIVATE m) + target_compile_options(aptdec-cli PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) endif() else() - MESSAGE(WARNING "Only building apt library, as not all of the required libraries were found for aptdec.") + message(WARNING "Not building aptdec-cli as some/all required dependencies are missing, libaptdec will still be built") endif() -if (MSVC) - target_compile_options(apt PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPT_API_EXPORT) - target_compile_options(aptstatic PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPT_API_STATIC) +# libaptdec +set(LIBAPTDEC_HEADER_FILES libaptdec/include/aptdec.h) +set(LIBAPTDEC_SOURCE_FILES + libaptdec/algebra.c + libaptdec/calibration.c + libaptdec/color.c + libaptdec/dsp.c + libaptdec/filter.c + libaptdec/image.c + libaptdec/util.c + libaptdec/effects.c +) +add_library(aptdec SHARED ${LIBAPTDEC_SOURCE_FILES}) + +set_target_properties(aptdec PROPERTIES PUBLIC_HEADER ${LIBAPTDEC_HEADER_FILES}) +target_include_directories(aptdec PRIVATE libaptdec/include/) +target_compile_definitions(aptdec PRIVATE "VERSION=\"${VERSION}\"") + +if(MSVC) + target_compile_options(aptdec PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPTDEC_API_EXPORT) else() - # Math - target_link_libraries(apt PRIVATE m) - target_link_libraries(aptstatic PRIVATE m) - - if(CMAKE_BUILD_TYPE MATCHES "Release") - target_compile_options(apt PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) - else() - target_compile_options(apt PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) - endif() + target_link_libraries(aptdec PRIVATE m) + target_compile_options(aptdec PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) endif() -# TODO: get this from git -set(PROJECT_VERSION "1.7.0") - -# CPack -set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +# Packaging +set(CPACK_PACKAGE_VERSION "${VERSION}") set(CPACK_PACKAGE_NAME "aptdec") set(CPACK_PACKAGE_CONTACT "Xerbo (xerbo@protonmail.com)") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "NOAA APT satellite imagery decoder") -set(CPACK_PACKAGE_DESCRIPTION "Aptdec is a FOSS program that decodes images transmitted by NOAA weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT.") -set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") +set(CPACK_PACKAGE_DESCRIPTION "Aptdec is a FOSS library/program that decodes images transmitted by the NOAA POES weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT.") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) -IF(NOT WIN32) +if(NOT WIN32) set(CPACK_GENERATOR "DEB;TGZ") set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}.${CMAKE_SYSTEM_PROCESSOR}") else() @@ -89,11 +94,10 @@ else() endif() endif() -if (TARGET aptdec) +if(TARGET aptdec-cli) install(TARGETS aptdec RUNTIME DESTINATION bin) - install(DIRECTORY "${PROJECT_SOURCE_DIR}/palettes" DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}) endif() -install(TARGETS apt PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/apt LIBRARY DESTINATION lib) +install(TARGETS aptdec PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/aptdec LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) include(CPack) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff8b624..988ccf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,28 +1,27 @@ # How To Contribute -Thank you for showing interest to contributing to aptdec, these guidelines layout how you should go about reporting issues and contributing. +Thanks for showing an interest in improving aptdec, these guidelines outline how you should go about contributing to the project. -## Did you Find A Bug? +## Did you find a bug? -1. Ensure the bug was not already reported by searching on GitHub under Issues. -2. If you're unable to find an closed issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible and an audio file that demonstrates what is unexpected behaviour. -3. If possible, use the report templates to create the issue. +1. Make sure the bug has not already been reported +2. If an issue already exists, leave a thumbs up to "bump" the issue +3. If an issue doesn't exist, open a new one with a clear title and description and, if relevant, any files that cause the behavior -## Do you have a patch that fixes a bug? +## Do you have a patch that fixes a bug/adds a feature? -1. Fork the repository and push your changes on a new branch. -2. Add your changes on that branch, make sure to use sensible commit names. -3. Open a new GitHub pull request to pull into master. -4. Ensure the pull request description clearly describes the problem and solution. Include the relevant issue number if applicable. +1. Fork this repository and put your changes on a *new branch* +2. Make sure to use sensible commit names +3. Open a new pull request into master + +If you don't have a GitHub account, you can email me the patch at `xerbo (at) protonmail (dot) com` ## Coding style -- Whitespaces should be denoted with tabs -- This is FOSS software, consider that people will read your code, so make it easily readable. -- If you're making major changes, make sure to create an issue to discuss it. +The coding style of LeanHRPT is based off the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) with minor modifications (see `.clang-format`), in addition to this all files should use LF line endings and end in a newline. Use American english (i.e. "color", not "colour"). ## Commit message style -- Keep titles under 80 characters to prevent wrapping. -- Make sure the commit message is descriptive of the change, dont be afraid to go into detail. -- Split up large changes into multiple commits. +- Keep titles short to prevent wrapping (descriptions exist) +- Split up large changes into multiple commits +- Never use hastags for sequential counting in commits, as this interferes with issue/PR referencing diff --git a/README.md b/README.md index 7865e84..1d8acab 100644 --- a/README.md +++ b/README.md @@ -6,58 +6,63 @@ Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 20 ## Introduction -Aptdec is a FOSS program that decodes images transmitted by NOAA weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT. -These transmissions can easily be received with a cheap SDR and simple antenna. Then the transmission can be decoded in narrow FM mode. +Aptdec is a FOSS library/program that decodes images transmitted by the NOAA POES weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT. +These transmissions can easily be received with a cheap SDR and simple antenna, the transmission can be demodulated in narrow FM mode. -Aptdec can turn the audio recordings into PNG images and generate images such as: +Aptdec can turn the audio into PNG images and generate other products such as: - - Raw image: both channels with full telemetry included - - Individual channel: one of the channels form the image - - Temperature image: a temperature compensated image derived from the IR channel - - Palleted image: a image where the color is derived from a palette (false color, etc) + - Raw image: both channels (including telemetry) + - Individual channel: one channel (including telemetry) + - Visible image: a calibrated visible image of either channel 1 or 2 + - Thermal image: a calibrated thermal image from channel B + - LUT image: a image where the color is derived from a LUT (used for false color, etc) -The input audio format can be anything supported by `libsndfile` (although only tested with WAV and FLAC). Sample rate doesn't matter, although lower samples rates will process faster. +The input audio format can be anything supported by `libsndfile` (although only tested with WAV, FLAC and Ogg Vorbis). While sample rate doesn't matter, it is recommended to use 16640 Hz (4x oversampling). ## Quick start +Grab a release from [Releases](https://github.com/Xerbo/aptdec/releases) or compile from source: + ```sh sudo apt install cmake git gcc libsndfile-dev libpng-dev git clone --recursive https://github.com/Xerbo/aptdec.git && cd aptdec cmake -B build cmake --build build -# Resulting binary is build/aptdec +# Resulting binary is build/aptdec-cli ``` +In place builds are not supported. + ## Examples -To create an image from `gqrx_20200527_115730_137914960.wav` (output filename will be `gqrx_20200527_115730_137914960-r.png`) +To create an image from `gqrx_20200527_115730_137914960.wav` (output filename will be `gqrx_20200527_115730_137914960-raw.png`) ```sh -./aptdec gqrx_20200527_115730_137914960.wav +aptdec-cli gqrx_20200527_115730_137914960.wav ``` -To manually set the output filename +To manually specify the output filename ```sh -./aptdec -o image.png gqrx_20200527_115730_137914960.wav +aptdec-cli -o image.png gqrx_20200527_115730_137914960.wav ``` -Decode all WAV files in the current directory and put them in `images` +Decode all WAV files in the current directory: ```sh -mkdir images && ./aptdec -d images *.wav +aptdec-cli *.wav ``` Apply a denoise filter (see [Post-Processing Effects](#post-processing-effects) for a full list of post-processing effects) ```sh -./aptdec -e d gqrx_20200527_115730_137914960.wav +aptdec-cli -e denoise gqrx_20200527_115730_137914960.wav ``` -Create a temperature compensated image for NOAA 18 +Create a calibrated IR image from NOAA 18 ```sh -./aptdec -i t -s 18 gqrx_20200527_115730_137914960.wav +aptdec-cli -i thermal gqrx_20200527_115730_137914960.wav ``` -Apply a falsecolor palette +Apply a falsecolor LUT ```sh -./aptdec -i p -p palettes/WXtoImg-N18-HVC.png gqrx_20200527_115730_137914960.wav +aptdec-cli -i lut -l luts/WXtoImg-N18-HVC.png gqrx_20200527_115730_137914960.wav ``` ## Usage @@ -65,72 +70,74 @@ Apply a falsecolor palette ### Arguments ``` --i [r|a|b|t|m|p] Output type (stackable) --e [t|h|l|d|p|f] Effects (stackable) --o Output filename --d Destination directory --s (15-19) Satellite number --p Path to palette --r Realtime decode --g Gamma adjustment (1.0 = off) +-h, --help show a help message and exit +-i, --image= set output image type (see below) +-e, --effect= add an effect (see below) +-g, --gamma= gamma adjustment (1.0 = off) +-s, --satellite= satellite ID, must be between 15, 18 or 19 or NORAD +-l, --lut= path to a LUT +-o, --output= path of output image +-r, --realtime decode in realtime ``` ### Image output types - - `r`: Raw Image - - `a`: Channel A - - `b`: Channel B - - `t`: Temperature - - `p`: Palleted + - `raw`: Raw Image + - `a`: Channel A (including telemetry) + - `b`: Channel B (including telemetry) + - `thermal`: Calibrated thermal (MWIR/LWIR) image + - `visible`: calibrated visible/NIR image + - `lut`: LUT image, see also `-l/--lut` ### Post-Processing Effects - - `t`: Crop telemetry (only effects raw image) - - `h`: Histogram equalise - - `l`: Linear equalise - - `d`: Denoise - - `p`: Precipitation overlay - - `f`: Flip image (for northbound passes) - - `c`: Crop noise from ends of image + - `strip`: Strip telemetry (only effects raw/a/b images) + - `equalize`: Histogram equalise + - `stretch`: Linear equalise + - `denoise`: Denoise + - ~~`precipitation`: Precipitation overlay~~ + - `flip`: Flip image (for northbound passes) + - `crop`: Crop noise from ends of image ## Realtime decoding -Aptdec even supports decoding in realtime. The following decodes the audio coming from the audio device `pulseaudio alsa_output.pci-0000_00_1b.0.analog-stereo` +Aptdec supports decoding in realtime. The following captures and decodes audio from the `pipewire` interface: +```sh +arecord -f cd -D pipewire | aptdec -r - ``` -mkfifo /tmp/aptaudio -aptdec -r /tmp/aptaudio -sox -t pulseaudio alsa_output.pci-0000_00_1b.0.analog-stereo.monitor -c 1 -t wav /tmp/aptaudio + +or directly from an SDR: + +```sh +rtl_fm -f 137.1M -g 40 -s 40k | sox -t raw -r 40k -e signed-integer -b 16 - -t wav - | aptdec -r - ``` -To stop the decode and calibrate the image simply kill the `sox` process. +Image data will be streamed to `CURRENT_TIME.png` (deleted when finished). To stop the decode and normalize the image simply `Ctrl+C` the process. -## Palette formatting +## LUT format -Palettes are just simple PNG images, 256x256px in size with 24bit RGB color. The X axis represents the value of Channel A and the Y axis the value of Channel B. +LUT's are just plain PNG images, 256x256px in size with 24bit RGB color. The X axis represents the value of Channel A and the Y axis the value of Channel B. ## Building for Windows -You can cross build for Windows from Linux with the `build_windows.sh` script, you will need the following: -``` +You can cross build for Windows from Linux (recommended) using with the following commands: +```sh sudo apt install wget cmake make mingw-w64 git unzip +./build_windows.sh ``` -To build natively on Windows using MSVC, you will also need: git, ninja and cmake. Then run: +To build natively on Windows using MSVC, you will need: git, ninja and cmake. Then run: ``` .\build_windows.bat ``` -If you just wish to build libaptdec on Windows, libpng and libsndfile aren't needed. - -## Further Reading - -[User's Guide for Building and Operating -Environmental Satellite Receiving Stations](https://noaasis.noaa.gov/NOAASIS/pubs/Users_Guide-Building_Receive_Stations_March_2009.pdf) +If you only want to build libaptdec, libpng and libsndfile aren't needed. -[NOAA KLM coefficients](https://web.archive.org/web/20141220021557/https://www.ncdc.noaa.gov/oa/pod-guide/ncdc/docs/klm/tables.htm) +## References -[NOAA Satellite specifications and more information](https://www1.ncdc.noaa.gov/pub/data/satellite/publications/podguides/N-15%20thru%20N-19/pdf/) +- [User's Guide for Building and Operating Environmental Satellite Receiving Stations](https://noaasis.noaa.gov/NOAASIS/pubs/Users_Guide-Building_Receive_Stations_March_2009.pdf) +- [NOAA KLM Users Guide](https://archive.org/details/noaa-klm-guide) ## License diff --git a/aptdec-cli/main.c b/aptdec-cli/main.c new file mode 100644 index 0000000..7441354 --- /dev/null +++ b/aptdec-cli/main.c @@ -0,0 +1,490 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#ifndef _MSC_VER +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +#include "argparse/argparse.h" +#include "pngio.h" +#include "util.h" + +// Maximum height of an APT image that can be decoded +#define APTDEC_MAX_HEIGHT 3000 + +#define STRING_SPLIT(src, dst, delim, n) \ +char *dst[n]; \ +size_t dst##_len = 0; \ +{char *token = strtok(src, delim); \ +while (token != NULL && dst##_len < n) { \ + dst[dst##_len++] = token; \ + token = strtok(NULL, delim); \ +}} + +typedef struct { + char *type; // Output image type + char *effects; // Effects on the image + int satellite; // The satellite number + int realtime; // Realtime decoding + char *filename; // Output filename + char *lut; // Filename of lut +} options_t; + +typedef struct { + SNDFILE *file; + SF_INFO info; +} SNDFILE_t; + +// Function declarations +static int process_file(const char *path, options_t *opts); +static int freq_from_filename(const char *filename); +static int satid_from_freq(int freq); +static size_t callback(float *samples, size_t count, void *context); +static void write_line(writer_t *png, float *row); + +static volatile int sigint_stop = 0; +void sigint_handler(int signum) { + (void)signum; + sigint_stop = 1; +} + +// Basename is unsupported by MSVC +// This implementation is GNU style +#ifdef _MSC_VER +char *basename(const char *filename) { + char *p = strrchr(filename, '/'); + return p ? p + 1 : (char *)filename; +} +#endif + +int main(int argc, const char **argv) { + char version[128]; + get_version(version); + printf("%s\n", version); + // clang-format off + options_t opts = { + .type = "raw", + .effects = "", + .satellite = 0, + .realtime = 0, + .filename = "", + .lut = "", + }; + // clang-format on + + static const char *const usages[] = { + "aptdec-cli [options] [[--] sources]", + "aptdec-cli [sources]", + NULL, + }; + + struct argparse_option options[] = { + OPT_HELP(), + OPT_GROUP("Image options"), + OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0), + OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0), + OPT_GROUP("Satellite options"), + OPT_INTEGER('s', "satellite", &opts.satellite, "satellite ID, must be either NORAD or between 15 and 19", NULL, 0, 0), + OPT_GROUP("Paths"), + OPT_STRING('l', "lut", &opts.lut, "path to a LUT", NULL, 0, 0), + OPT_STRING('o', "output", &opts.filename, "path of output image", NULL, 0, 0), + OPT_GROUP("Misc"), + OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0), + OPT_END(), + }; + + struct argparse argparse; + argparse_init(&argparse, options, usages, 0); + argparse_describe(&argparse, + "\nA lightweight FOSS NOAA APT satellite imagery decoder.", + "\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions." + ); + argc = argparse_parse(&argparse, argc, argv); + + if (argc == 0) { + argparse_usage(&argparse); + } + if (argc > 1 && opts.realtime) { + error("Cannot use -r/--realtime with multiple input files"); + } + + // Actually decode the files + for (int i = 0; i < argc; i++) { + if (opts.satellite == 25338) opts.satellite = 15; + if (opts.satellite == 28654) opts.satellite = 18; + if (opts.satellite == 33591) opts.satellite = 19; + + if (opts.satellite == 0) { + int freq = freq_from_filename(argv[i]); + if (freq == 0) { + opts.satellite = 19; + warning("Satellite not specified, defaulting to NOAA-19"); + } else { + opts.satellite = satid_from_freq(freq); + printf("Satellite not specified, choosing to NOAA-%i based on filename\n", opts.satellite); + } + } + + if (opts.satellite != 15 && opts.satellite != 18 && opts.satellite != 19) { + error("Invalid satellite ID"); + } + + process_file(argv[i], &opts); + } + + return 0; +} + +static int process_file(const char *path, options_t *opts) { + const char *path_basename = basename((char *)path); + const char *dot = strrchr(path_basename, '.'); + char name[256]; + if (dot == NULL) { + strncpy(name, path_basename, 255); + } else { + strncpy(name, path_basename, clamp_int(dot - path_basename, 0, 255)); + } + + // Set filename to time when reading from stdin + if (strcmp(name, "-") == 0) { + time_t t = time(NULL); + strcpy(name, ctime(&t)); + } + + writer_t *realtime_png; + if (opts->realtime) { + char filename[269]; + sprintf(filename, "%s-decoding.png", name); + realtime_png = writer_init(filename, APT_REGION_FULL, APTDEC_MAX_HEIGHT, PNG_COLOR_TYPE_GRAY, "Unknown"); + + // Capture Ctrl+C + signal(SIGINT, sigint_handler); + } + + // Create a libsndfile instance + SNDFILE_t audioFile; + audioFile.file = sf_open(path, SFM_READ, &audioFile.info); + if (audioFile.file == NULL) { + error_noexit("Could not open file"); + return 0; + } + + printf("Input file: %s\n", path_basename); + printf("Input sample rate: %d\n", audioFile.info.samplerate); + + // Create a libaptdec instances + aptdec_t *aptdec = aptdec_init(audioFile.info.samplerate); + if (aptdec == NULL) { + sf_close(audioFile.file); + error_noexit("Error initializing libaptdec, sample rate too high/low?"); + return 0; + } + + // Decode image + float *data = (float *)malloc(APT_IMG_WIDTH * (APTDEC_MAX_HEIGHT+1) * sizeof(float)); + size_t rows; + for (rows = 0; rows < APTDEC_MAX_HEIGHT; rows++) { + float *row = &data[rows * APT_IMG_WIDTH]; + + // Break the loop when there are no more samples or the process has been sent SIGINT + if (aptdec_getrow(aptdec, row, callback, &audioFile) == 0 || sigint_stop) { + break; + } + + if (opts->realtime) { + write_line(realtime_png, row); + } + + fprintf(stderr, "Row: %zu/%zu\r", rows+1, audioFile.info.frames/audioFile.info.samplerate * 2); + fflush(stderr); + } + printf("\n"); + + // Close stream + sf_close(audioFile.file); + aptdec_free(aptdec); + + if (opts->realtime) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" + writer_free(realtime_png); +#pragma GCC diagnostic pop + + char filename[269]; + sprintf(filename, "%s-decoding.png", name); + remove(filename); + } + + // Normalize + int error; + apt_image_t img = apt_normalize(data, rows, opts->satellite, &error); + if (error) { + error_noexit("Normalization failed"); + return 0; + } + + // clang-format off + const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" }; + const char *channel_desc[] = { + "unknown", + "visible (0.58-0.68 um)", + "near-infrared (0.725-1.0 um)", + "near-infrared (1.58-1.64 um)", + "thermal-infrared (10.3-11.3 um)", + "thermal-infrared (11.5-12.5 um)", + "mid-infrared (3.55-3.93 um)" + }; + + printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]); + printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]); + // clang-format on + + STRING_SPLIT(opts->type, images, ",", 10) + STRING_SPLIT(opts->effects, effects, ",", 10) + STRING_SPLIT(opts->filename, filenames, ",", 10) + + for (size_t i = 0; i < effects_len; i++) { + if (strcmp(effects[i], "crop") == 0) { + apt_crop(&img); + } else if (strcmp(effects[i], "denoise") == 0) { + apt_denoise(&img, APT_REGION_CHA); + apt_denoise(&img, APT_REGION_CHB); + } else if (strcmp(effects[i], "flip") == 0) { + apt_flip(&img, APT_REGION_CHA); + apt_flip(&img, APT_REGION_CHB); + } + } + + for (size_t i = 0; i < images_len; i++) { + const char *base; + if (i < filenames_len) { + base = filenames[i]; + } else { + base = name; + } + + if (strcmp(images[i], "thermal") == 0) { + if (img.ch[1] >= 4) { + char filename[269]; + sprintf(filename, "%s-thermal.png", base); + char description[128]; + sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]); + + // Perform visible calibration + apt_image_t _img = apt_image_clone(img); + apt_calibrate_thermal(&_img, APT_REGION_CHA); + + writer_t *writer = writer_init(filename, APT_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description); + writer_write_image_gradient(writer, &_img, temperature_gradient); + writer_free(writer); + + free(_img.data); + } else { + error_noexit("Could not generate thermal image, no infrared channel"); + } + } else if (strcmp(images[i], "visible") == 0) { + if (img.ch[0] <= 2) { + char filename[269]; + sprintf(filename, "%s-visible.png", base); + char description[128]; + sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]); + + // Perform visible calibration + apt_image_t _img = apt_image_clone(img); + apt_calibrate_visible(&_img, APT_REGION_CHA); + + writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description); + writer_write_image(writer, &_img); + writer_free(writer); + + free(_img.data); + } else { + error_noexit("Could not generate visible image, no visible channel"); + } + } + } + + for (size_t i = 0; i < effects_len; i++) { + if (strcmp(effects[i], "stretch") == 0) { + apt_stretch(&img, APT_REGION_CHA); + apt_stretch(&img, APT_REGION_CHB); + } else if (strcmp(effects[i], "equalize") == 0) { + apt_equalize(&img, APT_REGION_CHA); + apt_equalize(&img, APT_REGION_CHB); + } + } + + for (size_t i = 0; i < images_len; i++) { + const char *base; + if (i < filenames_len) { + base = filenames[i]; + } else { + base = name; + } + + if (strcmp(images[i], "raw") == 0) { + char filename[269]; + sprintf(filename, "%s-raw.png", base); + char description[128]; + sprintf(description, + "Raw image, channel %s - %s / %s - %s", + channel_name[img.ch[0]], + channel_desc[img.ch[0]], + channel_name[img.ch[1]], + channel_desc[img.ch[1]] + ); + + writer_t *writer = writer_init(filename, APT_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description); + writer_write_image(writer, &img); + writer_free(writer); + } else if (strcmp(images[i], "lut") == 0) { + if (opts->lut != NULL && opts->lut[0] != '\0') { + char filename[269]; + sprintf(filename, "%s-lut.png", base); + char description[128]; + sprintf(description, + "LUT image, channel %s - %s / %s - %s", + channel_name[img.ch[0]], + channel_desc[img.ch[0]], + channel_name[img.ch[1]], + channel_desc[img.ch[1]] + ); + + png_colorp lut = (png_colorp)malloc(sizeof(png_color)*256*256); + if (read_lut(opts->lut, lut)) { + writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description); + writer_write_image_lut(writer, &img, lut); + writer_free(writer); + } + free(lut); + } else { + warning("Cannot create LUT image, missing -l/--lut"); + } + } else if (strcmp(images[i], "a") == 0) { + char filename[269]; + sprintf(filename, "%s-a.png", base); + char description[128]; + sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]); + + writer_t *writer = writer_init(filename, APT_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description); + writer_write_image(writer, &img); + writer_free(writer); + } else if (strcmp(images[i], "b") == 0) { + char filename[269]; + sprintf(filename, "%s-b.png", base); + char description[128]; + sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]); + + writer_t *writer = writer_init(filename, APT_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description); + writer_write_image(writer, &img); + writer_free(writer); + } + } + + free(img.data); + return 1; +} + +static int freq_from_filename(const char *filename) { + char frequency_text[10] = {'\0'}; + + if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) { + memcpy(frequency_text, &filename[21], 9); + return atoi(frequency_text); + } else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) { + memcpy(frequency_text, &filename[26], 9); + return atoi(frequency_text); + } else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) { + memcpy(frequency_text, &filename[6], 9); + return atoi(frequency_text); + } + + return 0; +} + +static int satid_from_freq(int freq) { + int differences[3] = { + abs(freq - 137620000), // NOAA-15 + abs(freq - 137912500), // NOAA-18 + abs(freq - 137100000) // NOAA-19 + }; + + int best = 0; + for (size_t i = 0; i < 3; i++) { + if (differences[i] < differences[best]) { + best = i; + } + } + + const int lut[3] = {15, 18, 19}; + return lut[best]; +} + +// Read samples from a SNDFILE_t instance (passed through context) +static size_t callback(float *samples, size_t count, void *context) { + SNDFILE_t *file = (SNDFILE_t *)context; + + switch (file->info.channels) { + case 1: + return sf_read_float(file->file, samples, count); + case 2: { + float _samples[count * 2]; + size_t read = sf_read_float(file->file, _samples, count * 2); + + for (size_t i = 0; i < count; i++) { + // Average of left and right + samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f; + } + return read / 2; + } + default: + error_noexit("Only mono and stereo audio files are supported\n"); + return 0; + } +} + +// Write a line with very basic equalization +static void write_line(writer_t *png, float *row) { + float min = FLT_MAX; + float max = FLT_MIN; + for (int i = 0; i < APT_IMG_WIDTH; i++) { + if (row[i] < min) min = row[i]; + if (row[i] > max) max = row[i]; + } + + png_byte pixels[APT_IMG_WIDTH]; + for (int i = 0; i < APT_IMG_WIDTH; i++) { + pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" + png_write_row(png->png, pixels); +#pragma GCC diagnostic pop +} diff --git a/aptdec-cli/pngio.c b/aptdec-cli/pngio.c new file mode 100644 index 0000000..b267d12 --- /dev/null +++ b/aptdec-cli/pngio.c @@ -0,0 +1,153 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "pngio.h" + +#include +#include +#include + +#include "util.h" + +writer_t *writer_init(const char *filename, apt_region_t region, uint32_t height, int color, char *channel) { + writer_t *png = (writer_t *)malloc(sizeof(writer_t)); + png->region = region; + + // Create writer + png->png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png->png) { + error_noexit("Could not create PNG write struct"); + return NULL; + } + + png->info = png_create_info_struct(png->png); + if (!png->info) { + error_noexit("Could not create PNG info struct"); + return NULL; + } + + png_set_IHDR(png->png, png->info, png->region.width, height, 8, color, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + char version[128]; + int version_len = get_version(version); + + png_text text[] = { + {PNG_TEXT_COMPRESSION_NONE, "Software", version, version_len}, + {PNG_TEXT_COMPRESSION_NONE, "Channel", channel, strlen(channel)} + }; + png_set_text(png->png, png->info, text, 2); + + png->file = fopen(filename, "wb"); + if (!png->file) { + error_noexit("Could not open PNG file"); + return NULL; + } + png_init_io(png->png, png->file); + png_write_info(png->png, png->info); + + printf("Writing %s...\n", filename); + return png; +} + +void writer_write_image(writer_t *png, const apt_image_t *img) { + for (size_t y = 0; y < img->rows; y++) { + png_write_row(png->png, &img->data[y*APT_IMG_WIDTH + png->region.offset]); + } +} + +void writer_write_image_gradient(writer_t *png, const apt_image_t *img, const uint32_t *gradient) { + for (size_t y = 0; y < img->rows; y++) { + png_color pixels[APT_IMG_WIDTH]; + for (size_t x = 0; x < APT_IMG_WIDTH; x++) { + apt_rgb_t pixel = apt_gradient(gradient, img->data[y*APT_IMG_WIDTH + x + png->region.offset]); + pixels[x] = (png_color){ pixel.r, pixel.g, pixel.b }; + } + + png_write_row(png->png, (png_bytep)pixels); + } +} + +void writer_write_image_lut(writer_t *png, const apt_image_t *img, const png_colorp lut) { + for (size_t y = 0; y < img->rows; y++) { + png_color pixels[APT_CH_WIDTH]; + for (size_t x = 0; x < APT_CH_WIDTH; x++) { + uint8_t a = img->data[y*APT_IMG_WIDTH + x + APT_CHA_OFFSET]; + uint8_t b = img->data[y*APT_IMG_WIDTH + x + APT_CHB_OFFSET]; + pixels[x] = lut[b*256 + a]; + } + + png_write_row(png->png, (png_bytep)pixels); + } +} + +void writer_free(writer_t *png) { + png_write_end(png->png, png->info); + png_destroy_write_struct(&png->png, &png->info); + fclose(png->file); + free(png); +} + +int read_lut(const char *filename, png_colorp out) { + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + error_noexit("Could not create PNG read struct"); + return 0; + } + png_infop info = png_create_info_struct(png); + if (!info) { + error_noexit("Could not create PNG info struct"); + return 0; + } + + FILE *file = fopen(filename, "rb"); + if (!file) { + error_noexit("Cannot open LUT"); + return 0; + } + + png_init_io(png, file); + + // Read metadata + png_read_info(png, info); + uint32_t width = png_get_image_width(png, info); + uint32_t height = png_get_image_height(png, info); + png_byte color_type = png_get_color_type(png, info); + png_byte bit_depth = png_get_bit_depth(png, info); + + if (width != 256 && height != 256) { + error_noexit("LUT must be 256x256"); + return 0; + } + if (bit_depth != 8) { + error_noexit("LUT must be 8 bit"); + return 0; + } + if (color_type != PNG_COLOR_TYPE_RGB) { + error_noexit("LUT must be RGB"); + return 0; + } + + for (uint32_t i = 0; i < height; i++) { + png_read_row(png, (png_bytep)&out[i*width], NULL); + } + + png_read_end(png, info); + png_destroy_read_struct(&png, &info, NULL); + fclose(file); + return 1; +} diff --git a/aptdec-cli/pngio.h b/aptdec-cli/pngio.h new file mode 100644 index 0000000..e71d632 --- /dev/null +++ b/aptdec-cli/pngio.h @@ -0,0 +1,41 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef APTDEC_CLI_PNGIO_H_ +#define APTDEC_CLI_PNGIO_H_ + +#include +#include + +typedef struct { + png_structp png; + png_infop info; + FILE *file; + apt_region_t region; +} writer_t; + +writer_t *writer_init(const char *filename, apt_region_t region, uint32_t height, int color, char *channel); +void writer_free(writer_t *png); + +void writer_write_image(writer_t *png, const apt_image_t *img); +void writer_write_image_gradient(writer_t *png, const apt_image_t *img, const uint32_t *gradient); +void writer_write_image_lut(writer_t *png, const apt_image_t *img, const png_colorp lut); + +int read_lut(const char *filename, png_colorp out); + +#endif diff --git a/src/util.c b/aptdec-cli/util.c similarity index 83% rename from src/util.c rename to aptdec-cli/util.c index 612267a..64a3f32 100644 --- a/src/util.c +++ b/aptdec-cli/util.c @@ -1,6 +1,6 @@ /* * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ #include "util.h" +#include #include #include @@ -28,10 +29,12 @@ void error_noexit(const char *text) { fprintf(stderr, "\033[31mError: %s\033[0m\n", text); #endif } + void error(const char *text) { error_noexit(text); exit(1); } + void warning(const char *text) { #ifdef _WIN32 fprintf(stderr, "Warning: %s\r\n", text); @@ -40,10 +43,12 @@ void warning(const char *text) { #endif } -float clamp(float x, float hi, float lo) { +int clamp_int(int x, int lo, int hi) { if (x > hi) return hi; if (x < lo) return lo; return x; } -float clamp_half(float x, float hi) { return clamp(x, hi, -hi); } +int get_version(char *str) { + return sprintf(str, "aptdec-cli %s using libaptdec %s", VERSION, aptdec_get_version()); +} diff --git a/src/util.h b/aptdec-cli/util.h similarity index 68% rename from src/util.h rename to aptdec-cli/util.h index 7fd01ec..c8ab688 100644 --- a/src/util.h +++ b/aptdec-cli/util.h @@ -1,6 +1,6 @@ /* * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,19 +16,17 @@ * along with this program. If not, see . */ -#include -#define M_PIf 3.14159265358979323846f -#define M_TAUf (M_PIf * 2.0f) +#ifndef APTDEC_CLI_UTIL_H_ +#define APTDEC_CLI_UTIL_H_ -#define MIN(a, b) (((a) < (b)) ? (a) : (b)) -#define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#include -#ifndef UTIL_H -#define UTIL_H -float clamp(float x, float hi, float lo); -float clamp_half(float x, float hi); - -void error(const char *text); void error_noexit(const char *text); +__attribute__((noreturn)) void error(const char *text); void warning(const char *text); + +int clamp_int(int x, int lo, int hi); + +int get_version(char *str); + #endif diff --git a/build_windows.bat b/build_windows.bat index b51f2a2..259beaa 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -2,7 +2,7 @@ REM Build using Visual Studio 2019 on Windows REM Additional tools needed: git, cmake and ninja REM Build zlib -git clone https://github.com/madler/zlib +git clone -b v1.2.13 https://github.com/madler/zlib cd zlib mkdir build cd build @@ -11,7 +11,7 @@ ninja install cd ../../ REM Build libpng -git clone https://github.com/glennrp/libpng +git clone -b v1.6.39 https://github.com/glennrp/libpng cd libpng mkdir build cd build @@ -20,7 +20,7 @@ ninja install cd ../.. REM Build libsndfile - Could build Vorbis, FLAC and Opus first for extra support -git clone https://github.com/libsndfile/libsndfile +git clone -b 1.1.0 https://github.com/libsndfile/libsndfile cd libsndfile mkdir build cd build diff --git a/build_windows.sh b/build_windows.sh index 4cb578a..5d9558f 100755 --- a/build_windows.sh +++ b/build_windows.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Cross compile for Windows from Linux TEMP_PATH="$(pwd)/winpath" @@ -39,7 +39,6 @@ cp "libsndfile-1.0.29-win64/bin/sndfile.dll" $TEMP_PATH/bin/sndfile.dll cp "libsndfile-1.0.29-win64/include/sndfile.h" $TEMP_PATH/include/sndfile.h cp "libsndfile-1.0.29-win64/lib/sndfile.lib" $TEMP_PATH/lib/sndfile.lib - # Copy DLL's into root for CPack cp $TEMP_PATH/bin/*.dll ../ diff --git a/cmake/FindLibSndFile.cmake b/cmake/FindLibSndFile.cmake deleted file mode 100644 index 6b381b1..0000000 --- a/cmake/FindLibSndFile.cmake +++ /dev/null @@ -1,19 +0,0 @@ -# Find libsndfile -FIND_PATH(LIBSNDFILE_INCLUDE_DIR sndfile.h) - -SET(LIBSNDFILE_NAMES ${LIBSNDFILE_NAMES} sndfile libsndfile) -FIND_LIBRARY(LIBSNDFILE_LIBRARY NAMES ${LIBSNDFILE_NAMES} PATH) - -IF(LIBSNDFILE_INCLUDE_DIR AND LIBSNDFILE_LIBRARY) - SET(LIBSNDFILE_FOUND TRUE) -ENDIF(LIBSNDFILE_INCLUDE_DIR AND LIBSNDFILE_LIBRARY) - -IF(LIBSNDFILE_FOUND) - IF(NOT LibSndFile_FIND_QUIETLY) - MESSAGE(STATUS "Found LibSndFile: ${LIBSNDFILE_LIBRARY}") - ENDIF(NOT LibSndFile_FIND_QUIETLY) -ELSE(LIBSNDFILE_FOUND) - IF(LibSndFile_FIND_REQUIRED) - MESSAGE(FATAL_ERROR "Could not find sndfile") - ENDIF(LibSndFile_FIND_REQUIRED) -ENDIF(LIBSNDFILE_FOUND) diff --git a/src/algebra.c b/libaptdec/algebra.c similarity index 56% rename from src/algebra.c rename to libaptdec/algebra.c index bc7b9f6..e922a1e 100644 --- a/src/algebra.c +++ b/libaptdec/algebra.c @@ -1,6 +1,6 @@ /* * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ #include "algebra.h" +#include #include // Find the best linear equation to estimate the value of the @@ -51,6 +52,67 @@ linear_t linear_regression(const float *independent, const float *dependent, siz return (linear_t){a, b}; } -float linear_calc(float x, linear_t line) { return x * line.a + line.b; } +float standard_deviation(const float *data, size_t len) { + float mean = 0.0f; + for (size_t i = 0; i < len; i++) { + mean += data[i]; + } + mean /= (float)len; + + float deviation_mean = 0.0f; + for (size_t i = 0; i < len; i++) { + float deviation = data[i] - mean; + deviation_mean += deviation * deviation; + } + + return sqrtf(deviation_mean / (float)len); +} + +float sumf(const float *x, size_t len) { + float sum = 0.0f; + for (size_t i = 0; i < len; i++) { + sum += x[i]; + } + return sum; +} + +float meanf(const float *x, size_t len) { + return sumf(x, len) / (float)len; +} + +void normalizef(float *x, size_t len) { + float sum = sumf(x, len); + + for (size_t i = 0; i < len; i++) { + x[i] /= sum; + } +} + +static int sort_func(const void *a, const void *b) { + return *(float *)b > *(float *)a ? 1 : -1; +} + +float medianf(float *data, size_t len) { + qsort(data, len, sizeof(float), sort_func); -float quadratic_calc(float x, quadratic_t quadratic) { return x * x * quadratic.a + x * quadratic.b + quadratic.c; } + if (len % 2 == 0) { + return (data[len/2] + data[len/2 - 1]) / 2.0f; + } else { + return data[len/2]; + } +} + +float linear_calc(float x, linear_t line) { + return x * line.a + line.b; +} + +float quadratic_calc(float x, quadratic_t quadratic) { + return x*x * quadratic.a + x * quadratic.b + quadratic.c; +} + +float sincf(float x) { + if (x == 0.0f) { + return 1.0f; + } + return sinf(M_PIf * x) / (M_PIf * x); +} diff --git a/src/algebra.h b/libaptdec/algebra.h similarity index 71% rename from src/algebra.h rename to libaptdec/algebra.h index 08b46af..c7f654d 100644 --- a/src/algebra.h +++ b/libaptdec/algebra.h @@ -1,6 +1,6 @@ /* * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,10 +16,13 @@ * along with this program. If not, see . */ -#ifndef APTDEC_ALGEBRA_H -#define APTDEC_ALGEBRA_H +#ifndef LIBAPTDEC_ALGEBRA_H_ +#define LIBAPTDEC_ALGEBRA_H_ #include +#define M_PIf 3.14159265358979323846f +#define M_TAUf (M_PIf * 2.0f) + // A linear equation in the form of y(x) = ax + b typedef struct { float a, b; @@ -31,8 +34,16 @@ typedef struct { } quadratic_t; linear_t linear_regression(const float *independent, const float *dependent, size_t len); +float standard_deviation(const float *data, size_t len); +float sumf(const float *x, size_t len); +float meanf(const float *x, size_t len); +void normalizef(float *x, size_t len); + +// NOTE: Modifies input array +float medianf(float *data, size_t len); float linear_calc(float x, linear_t line); float quadratic_calc(float x, quadratic_t quadratic); +float sincf(float x); #endif diff --git a/libaptdec/calibration.c b/libaptdec/calibration.c new file mode 100644 index 0000000..0bec609 --- /dev/null +++ b/libaptdec/calibration.c @@ -0,0 +1,121 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "calibration.h" + +#include "util.h" + +// clang-format off +const calibration_t calibration[3] = { + { + .name = "NOAA-15", + .prt = { + { 1.36328e-06f, 0.051045f, 276.60157f }, // PRT 1 + { 1.47266e-06f, 0.050909f, 276.62531f }, // PRT 2 + { 1.47656e-06f, 0.050907f, 276.67413f }, // PRT 3 + { 1.47656e-06f, 0.050966f, 276.59258f } // PRT 4 + }, + .visible = { + { + .low = { 0.0568f, -2.1874f }, + .high = { 0.1633f, -54.9928f }, + .cutoff = 496.0f + }, { + .low = { 0.0596f, -2.4096f }, + .high = { 0.1629f, -55.2436f }, + .cutoff = 511.0f + } + }, + .rad = { + { 925.4075f, 0.337810f, 0.998719f }, // Channel 4 + { 839.8979f, 0.304558f, 0.999024f }, // Channel 5 + { 2695.9743f, 1.621256f, 0.998015f } // Channel 3B + }, + .cor = { + { -4.50f, { 0.0004524f, -0.0932f, 4.76f } }, // Channel 4 + { -3.61f, { 0.0002811f, -0.0659f, 3.83f } }, // Channel 5 + { 0.0f, { 0.0f, 0.0f , 0.0f } } // Channel 3B + } + }, { + .name = "NOAA-18", + .prt = { + { 1.657e-06f, 0.05090f, 276.601f }, // PRT 1 + { 1.482e-06f, 0.05101f, 276.683f }, // PRT 2 + { 1.313e-06f, 0.05117f, 276.565f }, // PRT 3 + { 1.484e-06f, 0.05103f, 276.615f } // PRT 4 + }, + .visible = { + { + .low = { 0.06174f, -2.434f }, + .high = { 0.1841f, -63.31f }, + .cutoff = 501.54f + }, { + .low = { 0.07514f, -2.960f }, + .high = { 0.2254f, -78.55f }, + .cutoff = 500.40f + } + }, + .rad = { + { 928.1460f, 0.436645f, 0.998607f }, // Channel 4 + { 833.2532f, 0.253179f, 0.999057f }, // Channel 5 + { 2659.7952f, 1.698704f, 0.996960f } // Channel 3B + }, + .cor = { + { -5.53f, { 0.00052337f, -0.11069f, 5.82f } }, // Channel 4 + { -2.22f, { 0.00017715f, -0.04360f, 2.67f } }, // Channel 5 + { 0.0f, { 0.0f, 0.0f, 0.0f } } // Channel 3B + } + }, { + .name = "NOAA-19", + .prt = { + { 1.405783e-06f, 0.051111f, 276.6067f }, // PRT 1 + { 1.496037e-06f, 0.051090f, 276.6119f }, // PRT 2 + { 1.496990e-06f, 0.051033f, 276.6311f }, // PRT 3 + { 1.493110e-06f, 0.051058f, 276.6268f } // PRT 4 + }, + .visible = { + { + .low = { 0.05555f, -2.159f }, + .high = { 0.1639f, -56.33f }, + .cutoff = 496.43f + }, { + .low = { 0.06614f, -2.565f }, + .high = { 0.1970f, -68.01f }, + .cutoff = 500.37f + } + }, + .rad = { + { 928.9f, 0.53959f, 0.998534f }, // Channel 4 + { 831.9f, 0.36064f, 0.998913f }, // Channel 5 + { 2670.0f, 1.67396f, 0.997364f } // Channel 3B + }, + .cor = { + { -5.49f, { 0.00054668f, -0.11187f, 5.70f } }, // Channel 4 + { -3.39f, { 0.00024985f, -0.05991f, 3.58f } }, // Channel 5 + { 0.0f, { 0.0f, 0.0f, 0.0f } } // Channel 3B + } + } +}; + +calibration_t get_calibration(apt_satellite_t satid) { + switch (satid) { + case NOAA15: return calibration[0]; + case NOAA18: return calibration[1]; + default: return calibration[2]; + } +} diff --git a/src/calibration.h b/libaptdec/calibration.h similarity index 87% rename from src/calibration.h rename to libaptdec/calibration.h index 5580dd6..46313e2 100644 --- a/src/calibration.h +++ b/libaptdec/calibration.h @@ -1,6 +1,6 @@ /* * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,9 +16,11 @@ * along with this program. If not, see . */ -#ifndef APTDEC_CALIBRATION_H -#define APTDEC_CALIBRATION_H +#ifndef LIBAPTDEC_CALIBRATION_H_ +#define LIBAPTDEC_CALIBRATION_H_ + #include "algebra.h" +#include typedef struct { char *name; @@ -50,6 +52,6 @@ static const float C1 = 1.1910427e-5f; // Second radiation constant (cm-K) static const float C2 = 1.4387752f; -calibration_t get_calibration(int satid); +calibration_t get_calibration(apt_satellite_t satid); #endif diff --git a/libaptdec/color.c b/libaptdec/color.c new file mode 100644 index 0000000..43bacb2 --- /dev/null +++ b/libaptdec/color.c @@ -0,0 +1,94 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include "util.h" + +#define MCOMPOSITE(m1, a1, m2, a2) (m1 * a1 + m2 * a2 * (1 - a1)) + +// clang-format off + +apt_rgb_t apt_gradient(const uint32_t *gradient, uint8_t val) { + return (apt_rgb_t) { + (gradient[val] & 0x00FF0000) >> 16, + (gradient[val] & 0x0000FF00) >> 8, + (gradient[val] & 0x000000FF) + }; +} + +apt_rgb_t apt_composite_rgb(apt_rgb_t top, float top_alpha, apt_rgb_t bottom, float bottom_alpha) { + return (apt_rgb_t) { + MCOMPOSITE(top.r, top_alpha, bottom.r, bottom_alpha), + MCOMPOSITE(top.g, top_alpha, bottom.g, bottom_alpha), + MCOMPOSITE(top.b, top_alpha, bottom.b, bottom_alpha) + }; +} + +// Taken from WXtoImg +const uint32_t temperature_gradient[256] = { + 0x45008F, 0x460091, 0x470092, 0x480094, 0x490096, 0x4A0098, 0x4B009B, + 0x4D009D, 0x4E00A0, 0x5000A2, 0x5100A5, 0x5200A7, 0x5400AA, 0x5600AE, + 0x5700B1, 0x5800B4, 0x5A00B7, 0x5C00BA, 0x5E00BD, 0x5F00C0, 0x6100C4, + 0x6400C8, 0x6600CB, 0x6800CE, 0x6900D1, 0x6800D4, 0x6500D7, 0x6300DA, + 0x6100DD, 0x5D00E1, 0x5B00E4, 0x5900E6, 0x5600E9, 0x5300EB, 0x5000EE, + 0x4D00F0, 0x4900F3, 0x4700FC, 0x4300FA, 0x3100BF, 0x200089, 0x200092, + 0x1E0095, 0x1B0097, 0x19009A, 0x17009C, 0x15009E, 0x1200A0, 0x0F00A3, + 0x0F02A5, 0x0E06A8, 0x0E0AAB, 0x0E0DAD, 0x0E11B1, 0x0D15B4, 0x0D18B7, + 0x0D1CBA, 0x0B21BD, 0x0A25C0, 0x0A29C3, 0x092DC6, 0x0833CA, 0x0736CD, + 0x073BD0, 0x0741D3, 0x0545D6, 0x044BD9, 0x0450DC, 0x0355DE, 0x025DE2, + 0x0161E5, 0x0066E7, 0x006CEA, 0x0072EC, 0x0078EE, 0x007DF0, 0x0082F3, + 0x008DFC, 0x0090FA, 0x0071BF, 0x005489, 0x005C91, 0x006194, 0x006496, + 0x006897, 0x006D99, 0x00719B, 0x00759D, 0x00799F, 0x007EA0, 0x0082A2, + 0x0087A4, 0x008CA6, 0x0092AA, 0x0096AC, 0x009CAE, 0x00A2B1, 0x00A6B3, + 0x00AAB5, 0x00ADB7, 0x00B1BA, 0x00B6BE, 0x00BAC0, 0x00BEC2, 0x00C2C5, + 0x00C6C6, 0x00CAC9, 0x00CCCA, 0x00CFCB, 0x00D2CC, 0x00D4CC, 0x00D6CC, + 0x00D9CB, 0x00DBCB, 0x00DECB, 0x00E0CB, 0x00E2CC, 0x00EAD2, 0x00EACF, + 0x00B9A4, 0x008E7A, 0x01947C, 0x049779, 0x079975, 0x099B71, 0x0D9D6B, + 0x109F67, 0x12A163, 0x15A35F, 0x17A559, 0x1AA855, 0x1DAA50, 0x20AC4B, + 0x24AF45, 0x28B241, 0x2BB53B, 0x2EB835, 0x31BA30, 0x34BD2B, 0x39BF24, + 0x3FC117, 0x49C508, 0x4FC801, 0x4FCA00, 0x4ECD00, 0x4ECF00, 0x4FD200, + 0x54D500, 0x5DD800, 0x68DB00, 0x6EDD00, 0x74DF00, 0x7AE200, 0x7FE400, + 0x85E700, 0x8BE900, 0x8FEB00, 0x9BF300, 0x9EF200, 0x7EBB00, 0x608A00, + 0x689200, 0x6D9500, 0x719600, 0x759800, 0x7B9A00, 0x7F9D00, 0x839F00, + 0x87A100, 0x8CA200, 0x8FA500, 0x92A700, 0x96A900, 0x9AAD00, 0x9DB000, + 0xA1B200, 0xA5B500, 0xA9B700, 0xADBA00, 0xB2BD00, 0xB6BF00, 0xBBC300, + 0xBFC600, 0xC3C800, 0xC8CB00, 0xCCCE00, 0xD0D100, 0xD3D200, 0xD5D400, + 0xD9D400, 0xDCD400, 0xDED500, 0xE1D500, 0xE3D500, 0xE6D400, 0xE8D100, + 0xEACE00, 0xF2CF00, 0xF2CA00, 0xBB9900, 0x8A6E00, 0x927200, 0x957200, + 0x977100, 0x9A7000, 0x9C6E00, 0x9E6D00, 0xA06B00, 0xA36A00, 0xA56800, + 0xA86700, 0xAB6600, 0xAE6500, 0xB26300, 0xB46100, 0xB75F00, 0xBA5D00, + 0xBD5C00, 0xC05900, 0xC35700, 0xC65400, 0xCA5000, 0xCD4D00, 0xD04A00, + 0xD34700, 0xD64300, 0xD94000, 0xDC3D00, 0xDE3900, 0xE23300, 0xE52F00, + 0xE72C00, 0xEA2800, 0xEC2300, 0xEF1F00, 0xF11A00, 0xF31400, 0xFB0F00, + 0xFA0D00, 0xC10500, 0x8E0000, 0x970000, 0x9B0000, 0x9E0000, 0xA10000, + 0xA50000, 0xA90000, 0xAD0000, 0xB10000, 0xB60000, 0xBA0000, 0xBD0000, + 0xC20000, 0xC80000, 0xCC0000, 0xCC0000 +}; + +// Taken from WXtoImg +const uint32_t precipitation_gradient[58] = { + 0x088941, 0x00C544, 0x00D12C, 0x00E31C, 0x00F906, 0x14FF00, 0x3EFF00, + 0x5DFF00, 0x80FF00, 0xABFF00, 0xCDFE00, 0xF8FF00, 0xFFE600, 0xFFB800, + 0xFF9800, 0xFF7500, 0xFF4900, 0xFE2600, 0xFF0400, 0xDF0000, 0xA80000, + 0x870000, 0x5A0000, 0x390000, 0x110000, 0x0E1010, 0x232222, 0x333333, + 0x414141, 0x535353, 0x606060, 0x6E6E6E, 0x808080, 0x8E8E8E, 0xA0A0A0, + 0xAEAEAE, 0xC0C0C0, 0xCECECE, 0xDCDCDC, 0xEFEFEF, 0xFAFAFA, 0xFFFFFF, + 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, + 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, + 0xFFFFFF, 0xFFFFFF +}; diff --git a/libaptdec/dsp.c b/libaptdec/dsp.c new file mode 100644 index 0000000..3c5f486 --- /dev/null +++ b/libaptdec/dsp.c @@ -0,0 +1,237 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "filter.h" +#include "util.h" +#include "algebra.h" + +#define BUFFER_SIZE 16384 +#define LOW_PASS_SIZE 101 + +#define CARRIER_FREQ 2400.0f +#define MAX_CARRIER_OFFSET 10.0f + +typedef struct { + float alpha; + float beta; + float min_freq; + float max_freq; + + float freq; + float phase; +} pll_t; + +typedef struct { + float *ring_buffer; + size_t ring_size; + + float *taps; + size_t ntaps; +} fir_t; + +struct aptdec_t { + float sample_rate; + float sync_frequency; + + pll_t *pll; + fir_t *hilbert; + + float low_pass[LOW_PASS_SIZE]; +}; + +char *aptdec_get_version(void) { + return VERSION; +} + +fir_t *fir_init(size_t max_size, size_t ntaps) { + fir_t *fir = (fir_t *)malloc(sizeof(fir_t)); + fir->ntaps = ntaps; + fir->ring_size = max_size + ntaps; + fir->taps = (float *)malloc(ntaps * sizeof(float)); + fir->ring_buffer = (float *)malloc((max_size + ntaps) * sizeof(float)); + return fir; +} + +void fir_free(fir_t *fir) { + free(fir->ring_buffer); + free(fir->taps); + free(fir); +} + +pll_t *pll_init(float alpha, float beta, float min_freq, float max_freq, float sample_rate) { + pll_t *pll = (pll_t *)malloc(sizeof(pll_t)); + pll->alpha = alpha; + pll->beta = beta; + pll->min_freq = M_TAUf * min_freq / sample_rate; + pll->max_freq = M_TAUf * max_freq / sample_rate; + pll->phase = 0.0f; + pll->freq = 0.0f; + return pll; +} + +aptdec_t *aptdec_init(float sample_rate) { + if (sample_rate > 96000 || sample_rate < (CARRIER_FREQ + APT_IMG_WIDTH) * 2.0f) { + return NULL; + } + + aptdec_t *apt = (aptdec_t *)malloc(sizeof(aptdec_t)); + apt->sample_rate = sample_rate; + apt->sync_frequency = 1.0f; + + // PLL configuration + // https://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html + float damp = 0.7f; + float bw = 0.005f; + float alpha = (4.0f * damp * bw) / (1.0f + 2.0f * damp * bw + bw * bw); + float beta = (4.0f * bw * bw) / (1.0f + 2.0f * damp * bw + bw * bw); + apt->pll = pll_init(alpha, beta, CARRIER_FREQ-MAX_CARRIER_OFFSET, CARRIER_FREQ+MAX_CARRIER_OFFSET, sample_rate); + if (apt->pll == NULL) { + free(apt); + return NULL; + } + + // Hilbert transform + apt->hilbert = fir_init(BUFFER_SIZE, 31); + if (apt->hilbert == NULL) { + free(apt->pll); + free(apt); + return NULL; + } + design_hilbert(apt->hilbert->taps, apt->hilbert->ntaps); + + design_low_pass(apt->low_pass, apt->sample_rate, (2080.0f + CARRIER_FREQ) / 2.0f, LOW_PASS_SIZE); + + return apt; +} + +void aptdec_free(aptdec_t *apt) { + fir_free(apt->hilbert); + free(apt->pll); + free(apt); +} + +static complexf_t pll_work(pll_t *pll, complexf_t in) { + // Internal oscillator (90deg offset) + complexf_t osc = complex_build(cosf(pll->phase), -sinf(pll->phase)); + in = complex_multiply(in, osc); + + // Error detector + float error = cargf(in); + + // Loop filter (single pole IIR) + pll->freq += pll->beta * error; + pll->freq = clamp(pll->freq, pll->min_freq, pll->max_freq); + pll->phase += pll->freq + (pll->alpha * error); + pll->phase = remainderf(pll->phase, M_TAUf); + + return in; +} + +static int am_demod(aptdec_t *apt, float *out, size_t count, aptdec_callback_t callback, void *context) { + size_t read = callback(&apt->hilbert->ring_buffer[apt->hilbert->ntaps], count, context); + + for (size_t i = 0; i < read; i++) { + complexf_t sample = hilbert_transform(&apt->hilbert->ring_buffer[i], apt->hilbert->taps, apt->hilbert->ntaps); + out[i] = crealf(pll_work(apt->pll, sample)); + } + + memcpy(apt->hilbert->ring_buffer, &apt->hilbert->ring_buffer[read], apt->hilbert->ntaps*sizeof(float)); + + return read; +} + +static int get_pixels(aptdec_t *apt, float *out, size_t count, aptdec_callback_t callback, void *context) { + static float buffer[BUFFER_SIZE]; + static size_t n = BUFFER_SIZE; + static float offset = 0.0; + + float ratio = apt->sample_rate / (4160.0f * apt->sync_frequency); + + for (size_t i = 0; i < count; i++) { + // Get more samples if there are less than `LOW_PASS_SIZE` available + if (n + LOW_PASS_SIZE > BUFFER_SIZE) { + memcpy(buffer, &buffer[n], (BUFFER_SIZE-n) * sizeof(float)); + + size_t read = am_demod(apt, &buffer[BUFFER_SIZE-n], n, callback, context); + if (read != n) { + return i; + } + n = 0; + } + + out[i] = interpolating_convolve(&buffer[n], apt->low_pass, LOW_PASS_SIZE, offset); + + // Do not question the sacred code + int shift = ceilf(ratio - offset); + offset = shift + offset - ratio; + n += shift; + } + + return count; +} + +const float sync_pattern[] = {-1, -1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, + 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 0}; +#define SYNC_SIZE (sizeof(sync_pattern)/sizeof(sync_pattern[0])) + +// Get an entire row of pixels, aligned with sync markers +int aptdec_getrow(aptdec_t *apt, float *row, aptdec_callback_t callback, void *context) { + static float pixels[APT_IMG_WIDTH + SYNC_SIZE + 2]; + + // Wrap the circular buffer + memcpy(pixels, &pixels[APT_IMG_WIDTH], (SYNC_SIZE + 2) * sizeof(float)); + // Get a lines worth (APT_IMG_WIDTH) of samples + if (get_pixels(apt, &pixels[SYNC_SIZE + 2], APT_IMG_WIDTH, callback, context) != APT_IMG_WIDTH) { + return 0; + } + + // Error detector + float left = FLT_MIN; + float middle = FLT_MIN; + float right = FLT_MIN; + size_t phase = 0; + + for (size_t i = 0; i < APT_IMG_WIDTH; i++) { + float _left = convolve(&pixels[i + 0], sync_pattern, SYNC_SIZE); + float _middle = convolve(&pixels[i + 1], sync_pattern, SYNC_SIZE); + float _right = convolve(&pixels[i + 2], sync_pattern, SYNC_SIZE); + if (_middle > middle) { + left = _left; + middle = _middle; + right = _right; + phase = i + 1; + } + } + + // Frequency + float bias = (left / middle) - (right / middle); + apt->sync_frequency = 1.0f + bias / APT_IMG_WIDTH / 2.0f; + + // Phase + memcpy(&row[APT_IMG_WIDTH], &pixels[phase], (APT_IMG_WIDTH - phase) * sizeof(float)); + memcpy(&row[APT_IMG_WIDTH - phase], pixels, phase * sizeof(float)); + + return 1; +} diff --git a/libaptdec/effects.c b/libaptdec/effects.c new file mode 100644 index 0000000..f8eff14 --- /dev/null +++ b/libaptdec/effects.c @@ -0,0 +1,175 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include "algebra.h" +#include "util.h" +#include "filter.h" + +void apt_equalize(apt_image_t *img, apt_region_t region) { + // Plot histogram + size_t histogram[256] = {0}; + for (size_t y = 0; y < img->rows; y++) { + for (size_t x = 0; x < region.width; x++) { + histogram[img->data[y * APT_IMG_WIDTH + x + region.offset]]++; + } + } + + // Calculate cumulative frequency + size_t sum = 0, cf[256] = {0}; + for (int i = 0; i < 256; i++) { + sum += histogram[i]; + cf[i] = sum; + } + + // Apply histogram + int area = img->rows * region.width; + for (size_t y = 0; y < img->rows; y++) { + for (size_t x = 0; x < region.width; x++) { + int k = (int)img->data[y * APT_IMG_WIDTH + x + region.offset]; + img->data[y * APT_IMG_WIDTH + x + region.offset] = (255.0f / area) * cf[k]; + } + } +} + +// Brightness calibrate, including telemetry +static void image_apply_linear(uint8_t *data, int rows, int offset, int width, linear_t regr) { + for (int y = 0; y < rows; y++) { + for (int x = 0; x < width; x++) { + float pv = linear_calc(data[y * APT_IMG_WIDTH + x + offset], regr); + data[y * APT_IMG_WIDTH + x + offset] = clamp_int(roundf(pv), 0, 255); + } + } +} + +void apt_stretch(apt_image_t *img, apt_region_t region) { + // Plot histogram + size_t histogram[256] = { 0 }; + for (size_t y = 0; y < img->rows; y++) { + for (size_t x = 0; x < region.width; x++) { + histogram[img->data[y*APT_IMG_WIDTH + x + region.offset]]++; + } + } + + // Calculate cumulative frequency + size_t sum = 0; + size_t cf[256] = { 0 }; + for (size_t i = 0; i < 256; i++) { + sum += histogram[i]; + cf[i] = sum; + } + + // Find min/max points (1st percentile) + int min = -1, max = -1; + for (size_t i = 0; i < 256; i++) { + if ((float)cf[i] / (float)sum < 0.01f && min == -1) { + min = i; + } + if ((float)cf[i] / (float)sum > 0.99f && max == -1) { + max = i; + break; + } + } + + float a = 255.0f / (max - min); + float b = a * -min; + image_apply_linear(img->data, img->rows, region.offset, region.width, (linear_t){a, b}); +} + + + +// Median denoise (with deviation threshold) +void apt_denoise(apt_image_t *img, apt_region_t region) { + for (size_t y = 1; y < img->rows - 1; y++) { + for (size_t x = 1; x < region.width - 1; x++) { + float pixels[9]; + int pixeln = 0; + for (int y2 = -1; y2 < 2; y2++) { + for (int x2 = -1; x2 < 2; x2++) { + pixels[pixeln++] = img->data[(y + y2) * APT_IMG_WIDTH + (x + region.offset) + x2]; + } + } + + if (standard_deviation(pixels, 9) > 15) { + img->data[y * APT_IMG_WIDTH + x + region.offset] = medianf(pixels, 9); + } + } + } +} + +// Flips a channel, for northbound passes +void apt_flip(apt_image_t *img, apt_region_t region) { + for (size_t y = 1; y < img->rows; y++) { + for (int x = 1; x < ceil(region.width / 2.0f); x++) { + // Flip top-left & bottom-right + swap_uint8( + &img->data[(img->rows - y) * APT_IMG_WIDTH + region.offset + x], + &img->data[y * APT_IMG_WIDTH + region.offset + (region.width - x)] + ); + } + } +} + +// Calculate crop to remove noise from the start and end of an image +#define NOISE_THRESH 2600.0 + +#include "filter.h" + +int apt_crop(apt_image_t *img) { + const float sync_pattern[] = {-1, -1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, + 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 0}; + + float spc_rows[img->rows]; + int startCrop = 0; + int endCrop = img->rows; + + for (size_t y = 0; y < img->rows; y++) { + float temp[39]; + for (size_t i = 0; i < 39; i++) { + temp[i] = img->data[y * APT_IMG_WIDTH + i]; + } + + spc_rows[y] = convolve(temp, &sync_pattern[0], 39); + } + + // Find ends + for (size_t y = 0; y < img->rows - 1; y++) { + if (spc_rows[y] > NOISE_THRESH) { + endCrop = y; + } + } + for (size_t y = img->rows; y > 0; y--) { + if (spc_rows[y] > NOISE_THRESH) { + startCrop = y; + } + } + + printf("Crop rows: %i -> %i\n", startCrop, endCrop); + + // Ignore the noisy rows at the end + img->rows = (endCrop - startCrop); + + // Remove the noisy rows at start + memmove(img->data, &img->data[startCrop * APT_IMG_WIDTH], img->rows * APT_IMG_WIDTH * sizeof(float)); + + return startCrop; +} diff --git a/libaptdec/filter.c b/libaptdec/filter.c new file mode 100644 index 0000000..e45fbf8 --- /dev/null +++ b/libaptdec/filter.c @@ -0,0 +1,109 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "filter.h" + +#include +// SSE2 intrinsics +#ifdef __x86_64__ +#include +#endif + +#include "algebra.h" + +// Blackman window +// https://en.wikipedia.org/wiki/Window_function#Blackman_window +static float blackman(float n, size_t ntaps) { + n = (M_PIf * n) / (float)(ntaps - 1); + return 0.42f - 0.5f*cosf(2 * n) + 0.08f*cosf(4 * n); +} + +// Sinc low pass with blackman window. +// https://tomroelandts.com/articles/how-to-create-a-simple-low-pass-filter +void design_low_pass(float *taps, float samp_rate, float cutoff, size_t ntaps) { + for (size_t i = 0; i < ntaps; i++) { + int x = i - ntaps/2; + taps[i] = sincf(2.0f * cutoff/samp_rate * (float)x); + taps[i] *= blackman(i, ntaps); + } + + // Achieve unity gain + normalizef(taps, ntaps); +} + +// Hilbert filter with blackman window. +// https://www.recordingblogs.com/wiki/hilbert-transform +void design_hilbert(float *taps, size_t ntaps) { + for (size_t i = 0; i < ntaps; i++) { + int x = i - ntaps/2; + + if (x % 2 == 0) { + taps[i] = 0.0f; + } else { + taps[i] = 2.0f / (M_PIf * (float)x); + taps[i] *= blackman(i, ntaps); + } + } + + // Achieve unity gain + normalizef(taps, ntaps); +} + +float convolve(const float *in, const float *taps, size_t len) { +#ifdef __SSE2__ + __m128 sum = _mm_setzero_ps(); + + size_t i; + for (i = 0; i < len - 3; i += 4) { + __m128 _taps = _mm_loadu_ps(&taps[i]); + __m128 _in = _mm_loadu_ps(&in[i]); + sum = _mm_add_ps(sum, _mm_mul_ps(_taps, _in)); + } + + float residual = 0.0f; + for (; i < len; i++) { + residual += in[i] * taps[i]; + } + + __attribute__((aligned(16))) float _sum[4]; + _mm_store_ps(_sum, sum); + return _sum[0] + _sum[1] + _sum[2] + _sum[3] + residual; +#else + float sum = 0.0f; + for (size_t i = 0; i < len; i++) { + sum += in[i] * taps[i]; + } + + return sum; +#endif +} + +complexf_t hilbert_transform(const float *in, const float *taps, size_t len) { + return complex_build(in[len / 2], convolve(in, taps, len)); +} + +float interpolating_convolve(const float *in, const float *taps, size_t len, float offset) { + float _taps[len]; + + for (size_t i = 0; i < len; i++) { + float next = (i == len-1) ? 0.0f : taps[i+1]; + _taps[i] = taps[i]*(1.0f-offset) + next*offset; + } + + return convolve(in, _taps, len); +} diff --git a/src/filter.h b/libaptdec/filter.h similarity index 66% rename from src/filter.h rename to libaptdec/filter.h index 124a59b..d80a7df 100644 --- a/src/filter.h +++ b/libaptdec/filter.h @@ -1,6 +1,6 @@ /* * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,15 +16,27 @@ * along with this program. If not, see . */ +#ifndef LIBAPTDEC_FILTER_H_ +#define LIBAPTDEC_FILTER_H_ + #include #include #ifdef _MSC_VER typedef _Fcomplex complexf_t; +#define complex_build(real, imag) _FCbuild(real, imag) +#define complex_multiply(a, b) _FCmulcc(a, b) #else typedef complex float complexf_t; +#define complex_build(real, imag) ((real) + (imag)*I) +#define complex_multiply(a, b) ((a) * (b)) #endif +void design_low_pass(float *taps, float samp_rate, float cutoff, size_t ntaps); +void design_hilbert(float *taps, size_t ntaps); + float convolve(const float *in, const float *taps, size_t len); complexf_t hilbert_transform(const float *in, const float *taps, size_t len); -float interpolating_convolve(const float *in, const float *taps, size_t len, float offset, float delta); +float interpolating_convolve(const float *in, const float *taps, size_t len, float offset); + +#endif diff --git a/libaptdec/image.c b/libaptdec/image.c new file mode 100644 index 0000000..7093563 --- /dev/null +++ b/libaptdec/image.c @@ -0,0 +1,236 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "algebra.h" +#include +#include "util.h" +#include "calibration.h" + +#define APT_COUNT_RATIO (1023.0f/255.0f) + +apt_image_t apt_image_clone(apt_image_t img) { + apt_image_t _img = img; + _img.data = (uint8_t *)malloc(APT_IMG_WIDTH * img.rows); + memcpy(_img.data, img.data, APT_IMG_WIDTH * img.rows); + return _img; +} + +static void decode_telemetry(const float *data, size_t rows, size_t offset, float *wedges) { + // Calculate row average + float telemetry_rows[rows]; + for (size_t y = 0; y < rows; y++) { + telemetry_rows[y] = meanf(&data[y*APT_IMG_WIDTH + offset + APT_CH_WIDTH], APT_TELEMETRY_WIDTH); + } + + // Calculate relative telemetry offset (via step detection, i.e. wedge 8 to 9) + size_t telemetry_offset = 0; + float max_difference = 0.0f; + for (size_t y = APT_WEDGE_HEIGHT; y <= rows - APT_WEDGE_HEIGHT; y++) { + float difference = sumf(&telemetry_rows[y - APT_WEDGE_HEIGHT], APT_WEDGE_HEIGHT) - sumf(&telemetry_rows[y], APT_WEDGE_HEIGHT); + + // Find the maximum difference + if (difference > max_difference) { + max_difference = difference; + telemetry_offset = (y + 64) % APT_FRAME_LEN; + } + } + + // Find the least noisy frame (via standard deviation) + float best_noise = FLT_MAX; + size_t best_frame = 0; + for (size_t y = telemetry_offset; y < rows; y += APT_FRAME_LEN) { + float noise = 0.0f; + for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { + noise += standard_deviation(&telemetry_rows[y + i*APT_WEDGE_HEIGHT], APT_WEDGE_HEIGHT); + } + + if (noise < best_noise) { + best_noise = noise; + best_frame = y; + } + } + + for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { + wedges[i] = meanf(&telemetry_rows[best_frame + i*APT_WEDGE_HEIGHT], APT_WEDGE_HEIGHT); + } +} + +static float average_spc(apt_image_t *img, size_t offset) { + float rows[img->rows]; + float average = 0.0f; + for (size_t y = 0; y < img->rows; y++) { + float row_average = 0.0f; + for (size_t x = 0; x < APT_SPC_WIDTH; x++) { + row_average += img->data[y*APT_IMG_WIDTH + offset - APT_SPC_WIDTH + x]; + } + row_average /= (float)APT_SPC_WIDTH; + + rows[y] = row_average; + average += row_average; + } + average /= (float)img->rows; + + float weighted_average = 0.0f; + size_t n = 0; + for (size_t y = 0; y < img->rows; y++) { + if (fabsf(rows[y] - average) < 50.0f) { + weighted_average += rows[y]; + n++; + } + } + + return weighted_average / (float)n; +} + +apt_image_t apt_normalize(const float *data, size_t rows, apt_satellite_t satellite, int *error) { + apt_image_t img; + img.rows = rows; + img.satellite = satellite; + + *error = 0; + if (rows < APTDEC_NORMALIZE_ROWS) { + *error = -1; + return img; + } + + // Decode and average wedges + float wedges[APT_FRAME_WEDGES]; + float wedges_cha[APT_FRAME_WEDGES]; + float wedges_chb[APT_FRAME_WEDGES]; + decode_telemetry(data, rows, APT_CHA_OFFSET, wedges_cha); + decode_telemetry(data, rows, APT_CHB_OFFSET, wedges_chb); + for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { + wedges[i] = (wedges_cha[i] + wedges_chb[i]) / 2.0f; + } + + // Calculate normalization + const float reference[9] = { 31, 63, 95, 127, 159, 191, 223, 255, 0 }; + linear_t normalization = linear_regression(wedges, reference, 9); + if (normalization.a < 0.0f) { + *error = -1; + return img; + } + + // Normalize telemetry + for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { + img.telemetry[0][i] = linear_calc(wedges_cha[i], normalization); + img.telemetry[1][i] = linear_calc(wedges_chb[i], normalization); + } + + // Decode channel ID wedges + img.ch[0] = roundf(img.telemetry[0][15] / 32.0f); + img.ch[1] = roundf(img.telemetry[1][15] / 32.0f); + if (img.ch[0] < 1 || img.ch[0] > 6) img.ch[0] = AVHRR_CHANNEL_UNKNOWN; + if (img.ch[1] < 1 || img.ch[1] > 6) img.ch[1] = AVHRR_CHANNEL_UNKNOWN; + + // Normalize and quantize image data + img.data = (uint8_t *)malloc(rows * APT_IMG_WIDTH); + for (size_t i = 0; i < rows * APT_IMG_WIDTH; i++) { + float count = linear_calc(data[i], normalization); + img.data[i] = clamp_int(roundf(count), 0, 255); + } + + // Get space brightness + img.space_view[0] = average_spc(&img, APT_CHA_OFFSET); + img.space_view[1] = average_spc(&img, APT_CHB_OFFSET); + + return img; +} + +static void make_thermal_lut(apt_image_t *img, avhrr_channel_t ch, int satellite, float *lut) { + ch -= 4; + const calibration_t calibration = get_calibration(satellite); + const float Ns = calibration.cor[ch].Ns; + const float Vc = calibration.rad[ch].vc; + const float A = calibration.rad[ch].A; + const float B = calibration.rad[ch].B; + + // Compute PRT temperature + float T[4]; + for (size_t n = 0; n < 4; n++) { + T[n] = quadratic_calc(img->telemetry[1][n + 9] * APT_COUNT_RATIO, calibration.prt[n]); + } + + float Tbb = meanf(T, 4); // Blackbody temperature + float Tbbstar = A + Tbb * B; // Effective blackbody temperature + + float Nbb = C1 * pow(Vc, 3) / (expf(C2 * Vc / Tbbstar) - 1.0f); // Blackbody radiance + + float Cs = img->space_view[1] * APT_COUNT_RATIO; + float Cb = img->telemetry[1][14] * APT_COUNT_RATIO; + + for (size_t i = 0; i < 256; i++) { + float Nl = Ns + (Nbb - Ns) * (Cs - i * APT_COUNT_RATIO) / (Cs - Cb); // Linear radiance estimate + float Nc = quadratic_calc(Nl, calibration.cor[ch].quadratic); // Non-linear correction + float Ne = Nl + Nc; // Corrected radiance + + float Testar = C2 * Vc / logf(C1 * powf(Vc, 3) / Ne + 1.0); // Equivalent black body temperature + float Te = (Testar - A) / B; // Temperature (kelvin) + + // Convert to celsius + lut[i] = Te - 273.15; + } +} + +int apt_calibrate_thermal(apt_image_t *img, apt_region_t region) { + if (img->ch[1] != AVHRR_CHANNEL_4 && img->ch[1] != AVHRR_CHANNEL_5 && img->ch[1] != AVHRR_CHANNEL_3B) { + return 1; + } + + float lut[256]; + make_thermal_lut(img, img->ch[1], img->satellite, lut); + + for (size_t y = 0; y < img->rows; y++) { + for (size_t x = 0; x < region.width; x++) { + float temperature = lut[img->data[y * APT_IMG_WIDTH + region.offset + x]]; + img->data[y * APT_IMG_WIDTH + region.offset + x] = clamp_int(roundf((temperature + 100.0) / 160.0 * 255.0), 0, 255); + } + } + + return 0; +} + +static float calibrate_pixel_visible(float value, int channel, calibration_t cal) { + if (value > cal.visible[channel].cutoff) { + return linear_calc(value * APT_COUNT_RATIO, cal.visible[channel].high) / 100.0f * 255.0f; + } else { + return linear_calc(value * APT_COUNT_RATIO, cal.visible[channel].low) / 100.0f * 255.0f; + } +} + +int apt_calibrate_visible(apt_image_t *img, apt_region_t region) { + if (img->ch[0] != AVHRR_CHANNEL_1 && img->ch[0] != AVHRR_CHANNEL_2) { + return 1; + } + + calibration_t calibration = get_calibration(img->satellite); + for (size_t y = 0; y < img->rows; y++) { + for (size_t x = 0; x < region.width; x++) { + float albedo = calibrate_pixel_visible(img->data[y * APT_IMG_WIDTH + region.offset + x], img->ch[0]-1, calibration); + img->data[y * APT_IMG_WIDTH + region.offset + x] = clamp_int(roundf(albedo), 0, 255); + } + } + + return 0; +} diff --git a/libaptdec/include/aptdec.h b/libaptdec/include/aptdec.h new file mode 100644 index 0000000..669537c --- /dev/null +++ b/libaptdec/include/aptdec.h @@ -0,0 +1,169 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) + * Copyright (C) 2021 Jon Beniston (M7RCE) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef APTDEC_H_ +#define APTDEC_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__GNUC__) && (__GNUC__ >= 4) +# define APTDEC_API __attribute__((visibility("default"))) +#elif defined(_MSC_VER) +# ifdef APTDEC_API_EXPORT +# define APTDEC_API __declspec(dllexport) +# else +# define APTDEC_API __declspec(dllimport) +# endif +#else +# define APTDEC_API +#endif + +// Height of a single telemetry wedge +#define APT_WEDGE_HEIGHT 8 +// Numbers of wedges in a frame +#define APT_FRAME_WEDGES 16 +// Height of a telemetry frame +#define APT_FRAME_LEN (APT_WEDGE_HEIGHT * APT_FRAME_WEDGES) + +// Width of the overall image +#define APT_IMG_WIDTH 2080 +// Width of sync marker +#define APT_SYNC_WIDTH 39 +// Width of space view +#define APT_SPC_WIDTH 47 +// Width of telemetry +#define APT_TELEMETRY_WIDTH 45 +// Width of a single video channel +#define APT_CH_WIDTH 909 + +// Offset to channel A video data +#define APT_CHA_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH) +// Offset to channel B video data +#define APT_CHB_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_CH_WIDTH + APT_TELEMETRY_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH) + +// Number of rows needed for apt_normalize to (reliably) work +#define APTDEC_NORMALIZE_ROWS (APT_FRAME_LEN * 2) + +// Channel 1: visible (0.58-0.68 um) +// Channel 2: near-IR (0.725-1.0 um) +// Channel 3A: near-IR (1.58-1.64 um) +// Channel 3B: mid-infrared (3.55-3.93 um) +// Channel 4: thermal-infrared (10.3-11.3 um) +// Channel 5: thermal-infrared (11.5-12.5 um) +typedef enum apt_channel { + AVHRR_CHANNEL_UNKNOWN, + AVHRR_CHANNEL_1, + AVHRR_CHANNEL_2, + AVHRR_CHANNEL_3A, + AVHRR_CHANNEL_4, + AVHRR_CHANNEL_5, + AVHRR_CHANNEL_3B +} avhrr_channel_t; + +typedef enum apt_satellite { + NOAA15, + NOAA18, + NOAA19 +} apt_satellite_t; + +typedef struct { + uint8_t *data; // Image data + size_t rows; // Number of rows + + // Telemetry + apt_satellite_t satellite; + avhrr_channel_t ch[2]; + float space_view[2]; + float telemetry[2][16]; +} apt_image_t; + +typedef struct { + uint8_t r, g, b; +} apt_rgb_t; + +typedef struct { + size_t offset; + size_t width; +} apt_region_t; + +typedef struct aptdec_t aptdec_t; + +// Callback function to get samples +// `context` is the same as passed to aptdec_getrow +typedef size_t (*aptdec_callback_t)(float *samples, size_t count, void *context); + +// Clone an apt_image_t struct +// Useful for calibration +apt_image_t apt_image_clone(apt_image_t img); + +// Returns version of libaptdec in git tag format +// i.e. v2.0.0 or v2.0.0-1-xxxxxx +APTDEC_API char *aptdec_get_version(void); + +// Create and destroy libaptdec instances +// If aptdec_init fails it will return NULL +APTDEC_API aptdec_t *aptdec_init(float sample_rate); +APTDEC_API void aptdec_free(aptdec_t *apt); + +// Normalize and quantize raw image data +// Data is arranged so that each row starts at APT_IMG_WIDTH*y +APTDEC_API apt_image_t apt_normalize(const float *data, size_t rows, apt_satellite_t satellite, int *error); + +// Get an entire row of pixels +// Requires that `row` has enough space to store APT_IMG_WIDTH*2 +// Returns 0 when `callback` return value != count +APTDEC_API int aptdec_getrow(aptdec_t *apt, float *row, aptdec_callback_t callback, void *context); + +// Calibrate channels +APTDEC_API int apt_calibrate_thermal(apt_image_t *img, apt_region_t region); +APTDEC_API int apt_calibrate_visible(apt_image_t *img, apt_region_t region); + +APTDEC_API void apt_denoise (apt_image_t *img, apt_region_t region); +APTDEC_API void apt_flip (apt_image_t *img, apt_region_t region); +APTDEC_API void apt_stretch (apt_image_t *img, apt_region_t region); +APTDEC_API void apt_equalize(apt_image_t *img, apt_region_t region); +APTDEC_API int apt_crop (apt_image_t *img); + +// Composite two RGB values as layers, in most cases bottom_a will be 1.0f +APTDEC_API apt_rgb_t apt_composite_rgb(apt_rgb_t top, float top_a, apt_rgb_t bottom, float bottom_a); + +// Apply a gradient such as temperature_gradient +// If gradient is less than 256 elements it is the callers responsibility +// that `val` does not exceed the length of the gradient +APTDEC_API apt_rgb_t apt_gradient(const uint32_t *gradient, uint8_t val); + +static const apt_region_t APT_REGION_CHA = { APT_CHA_OFFSET, APT_CH_WIDTH }; +static const apt_region_t APT_REGION_CHB = { APT_CHB_OFFSET, APT_CH_WIDTH }; +static const apt_region_t APT_REGION_CHA_FULL = { 0, APT_IMG_WIDTH/2 }; +static const apt_region_t APT_REGION_CHB_FULL = { APT_IMG_WIDTH/2, APT_IMG_WIDTH/2 }; +static const apt_region_t APT_REGION_FULL = { 0, APT_IMG_WIDTH }; + +extern const uint32_t temperature_gradient[256]; +extern const uint32_t precipitation_gradient[58]; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libaptdec/util.c b/libaptdec/util.c new file mode 100644 index 0000000..e2139bb --- /dev/null +++ b/libaptdec/util.c @@ -0,0 +1,40 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "util.h" + +#include +#include + +float clamp(float x, float lo, float hi) { + if (x > hi) return hi; + if (x < lo) return lo; + return x; +} + +int clamp_int(int x, int lo, int hi) { + if (x > hi) return hi; + if (x < lo) return lo; + return x; +} + +void swap_uint8(uint8_t *a, uint8_t *b) { + uint8_t tmp = *a; + *a = *b; + *b = tmp; +} diff --git a/libaptdec/util.h b/libaptdec/util.h new file mode 100644 index 0000000..fbb1e1d --- /dev/null +++ b/libaptdec/util.h @@ -0,0 +1,29 @@ +/* + * aptdec - A lightweight FOSS (NOAA) APT decoder + * Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIBAPTDEC_UTIL_H_ +#define LIBAPTDEC_UTIL_H_ + +#include + +float clamp(float x, float lo, float hi); +int clamp_int(int x, int lo, int hi); + +void swap_uint8(uint8_t *a, uint8_t *b); + +#endif diff --git a/luts/N19-HRPT-Falsecolor.png b/luts/N19-HRPT-Falsecolor.png new file mode 100644 index 0000000000000000000000000000000000000000..eee6d89e968a8c6df0c20eb8fca772144df84f5e GIT binary patch literal 16186 zcmdtJRa9KT_a@pva7_sA?gV!Y1h?RBjRbFKT!KRg7Thg(j_cZY=*gBKh7Z4yP(t|Ng zZ_JRrSdTgW*!x~PQELOd@il>6?rG4iZA`ZQaFM?#KT9H#FhXUbw6i{X=upLYl+>w4EK%dat)jC-X+-cOlJSBJ1Bv2>dF9sH$4Cl5CQ-^yomz# z001`*0N}tB01!$A00=;ttzSjnDv-<-Wu*bH|G9Gi6eqk%P(gBfU;qFe_dhorAU%^1 z01yGlOG|2cE*}3eh_%#l*}aX_eavj1&0>uovoc{UE$sjH?`9AQ11J)JiNYa5INC;m zd1uxUxAsI9<#m?9nP1#!ug5l&EL~H2Lcd@iuhj<`E*}-yJ^GVO;ofIn{^w>~;ij5h zxl)2E&h5hRMv&o!h5E%Ax0$WBSm;N&mIpP2So*i^hxb}|5fEnc*MtVhPmWc-RSWS1 zIW@dE+NlL@$`6_~KKJ3$aY?rIOWG$xPb)#96P^w)sz&Dz%+z^Lr=CPv+P8Iz6|Y>z zQ`0%r8#(R1!U=~H{5wqna_Yy@@&0(X>!Tf^=MG#Rp)Y|O9VmFHH*{DxnItomQ<6Hh^za1#r3Y2}l9-|F;MbBogQ`0m|t-03Mquz~2J-|1RjXbJ_xY)UI|| zvrBNI1Oia$$+?aT`YLz;Pk)nX(v%$t(1rHyqmXX#6;SxJ zPJb;BL5}K4ma_1Wb2SuqMi#If?iMxog&I(t3+Ys_@r}=@4xBRugkD+^rMF81T&uN2 zL#5(;I9IC?UWiG%jidObv%83S`VX0mkWGQu{$zZh-w%|96rTfdo0F&5X1MPf263B_ zM6al0&3!RX`tD>DoCZ7z8bN?q%7&`Wh>j>7-}tFpphyTd48^gV7;7B^Ne*J*32!i8 zV*Dc$4(UT63bhN`4_~BM82-CBTtXPH?n2hBsmV3@veq+6+^wd6`jj0(pJGP}o(Z*f+GAJ6!F z0k<$`jToeW*N;Sac8Fa>nNc-W!X^Vi;q*g_qi++){?#l>SYPv={MtL`!8a=Vmt{wz zDCL;Z*z4$?WC6mEoNm;11b(Wn!>}B!=~$$IHT{@%s>~j=4@c4UQ44TKUF1gr6H5No z1|1k)WALWgs$wh12qa;@1#VAz`;Z<%m^U!0t22&%@eqXh;H9&Owq4*|JCEhtK&)gg zuEvH_$tsH3J%?qc`4TN%v=_HqJ~7>BvW&p(UWy5zQ_^8D5>3E0<|gY3Xg>=cL5azs zWtypz9BBr(*->9Cm4Gv>c<7l_%d5H*(!v8}a^9`S^?{WP*!h_5F6YGwqhaZD z$;xlr&Fi^llST@Cc76w9S8s*e{VVvPRK5;iGE6)!0mD%ly%!8-vsaD|dY@}I{Vxux zN$^2hcycb8Zm`DQwY<}#o@=B)#?o7Dog)E*V<;^M?2&`e%<+q~tJG&@MK^sCEu0u& zWohB5QeGsn-gxlmUR`tYt!iHup^L88df+uXE8Sq)rAi3uR&u4{A&Xt`>F_uB z2dn!ox5mIKkEd(ltYvPLV%j#XJ|7K~(%YU;jqvb{JVu9n(|H@`Mh330h!GRekeS*k z)|F?oI_n#66cMQQ!e7=V@CXwmHM}<1ZBI-hTly1d?SMUjI0J00h;*iKcYhrhp@2~r z3ZKOs;0gp_K1CnU%!eZoe0}o+OJ@M`G`oKVa4_KKsHWXL$Mz87L zapIpvQ4;0v#t9y?!SOGago8}`K!5Nu-Ot{QuOpH6dA@@msMdH6HKXqmv|mTR15lHu zQQ1xb&i(-x__1kql`qudV+AkqjyerIT%3#uIUpDPM`5jx79W_pG3SF31Oh(6P{uKV zz<9fisrTh$-lV;E_?g{#RO__;Busx$;lx6~GqF^92M@e=gpNIehT$Y2g4NKaGiQY+ zeeGtru7Mv*Q%egi@!4ir82KKFcBC$)6XTPKKz!EVykMFU!l;!f;dvoCTmKAK5U#203v;dp+MNL$d-CJ!7ed}nl- z`dLT*ZdsrL(2sB41_S6W+cc%$Drs8!;T~-X9ptxy3#c&ZL_$y zG}Cd@Xjc;vAXtkCW5))gl3@f_tVM)74ROzCmH#rSrQ3@?dm(=|eW{MS={vRxRhJ+4 zdqKNLH8%unkgZoR+p@F6wGW8;0%cWhpRwQWjPRuYj_+wc$TL)y@=TBv`bo>*jOjQs zF$VY)oVXf&-bPK)dpDu6=(Kow{H`n8ZslhT%>>rO>||E(b7zl>Jzow!I{V6ZRG9!KAISVPuUQR*9GqS!2TTN%*9uFd8u`o)HG`{L& z|3cUpZ*VjdQxCxtsi95L{bIzU!SO-*5g!nxO-)q!Z))n1#w2Yj3+Fo%H6-{4L=1-r zMi^s?9{WX1Jt^@!N=}4`rfBHgx>!40fh&JMPB#Nc`C7c>1+hJ8YKiLU^Hx6LD`iM$ z#JNexS!BypKK&XpCSZ&NHg*N@bmr90~9c&oQyCJqnd z=4QHu;XfH}O%s|Rz=9LsiOKCN&vfG4OJ7@4tEzj`^q)pP(f4PZDRO@Tb{u67B9^b~ zTA+IXpK@S%gUHn4p(dr--DES^WZR2dONaq0%Z6E6M9fNe6so}|7zln^!aRw7Cx6Z> zQk9znFhy$Sf%Qmhf#hPLKu?O*KoW@c-<~!Q&StE%SokRv|2sf_i)O~8OaGb%?1|Kj zY29=CeDYL5zHl1i3433M{6c5$#f-}nkZUb_%@3+v3)(msa*1tDPklL zcnYOjmpz+16D|AjEGvL_pEfuF!>0P_OTKwpcvXcFxRm%p(ma>AzXXJfCt1cB4)#^Q z;!6u?bE9%%u8H%5XxUiE2sU(wWNI}BR7}bO6GdW1{BRw6q?#Sb|IL?Bf=rXdeYBlj zv6^5X#R6VkNQse)&uH#G{vK3ak6l-Vc@n;mJ&8j)!;G(`w?y|~64$`E9OzPo^U+(2 zy@7fD$#pz-$VhW|u*o37<>0pN)_UOtY0c|rTm>7vZ^j3TDEFRr>zj{$jYIxs1`a#|O{;3O};l06G;K<^~k zM0)`OLproLyQHT`HR-)-NRge60!~+2vUhoDQ|Ol#-<55pPY+byENvz(i`ZiEf!?tQ zG|{f0F4rh^i-${jVyp!kB+L4I*qmQ3mw$w`ls$vEH?CVJgKW+a8|f1j4<~KPCrCr^59&7`PUDFYo-FF=qZGK{ zcVqr!j8eSm>h|Ld3*e7}4V#x4SP55!5w13d^n6qOj(wy4kl>ta9wn~rSMYGEh5J;% z``35VD0iX60~F1w+c&O;)sGf4aD{pFqDsVJ+Y$tYHwmRdH($>bw0LF-_%)qgN`A++-mvsfz42e_OJv zq7`X#GW>p=4SEk$2TttH&bSS<^1s_BuT2a4<2BT_l} z=ne3Yej#^ zy(sX#cHTJ|Ha?8ujT3cE+ebbV^u@|LMn`b$LUQb`Iw~cXLgojOP8(Bnd%gMmr-(j0 z3a9~yq2EnGyn1|evhzWhkbm0u_XB{u3;+A1`G%7P#xJsL&Ft+V7cKLzlC0rfrFbrm zSp%Jo-*245964@n>|K|e+9k6C;u%Ph_*J(XImHLgP=eybX=cFom;N_w=7J%rFk#S# z&C~ERL_^)G=b8gu<8!Q|h$o?tSN_6RxRY)~Qc1PvkEkaXu4ExOdJcvJo191HzKUC{{ldANF8~!-_Ml0GZ_xum*b~-w zx(kxKzkGlAJoZR7;OTY)r8DaG?KYo23GbpuoO3|0YPqfDqoFdfa8<9IPJ zZr0A#CVHl^kH={Syy2T*5zexk+Ov`thJbvgm1=9x%qeVGLR<;ny+FT*lqX6Xp%bKt zFji_F;s{GVDAsmp)p|y5E^G~MTDM%Mb$n_s*&hM|bXiBNjmxLk@#Y|ygt@!Rs@xSm5bok_Qo|KXU zm@0tF8knH$DPG-y`LarL5i~e~5BH-;z~f?9?vQYhvPBuOG?H#{y;ENI^h!Tdw9?n0 zgv5?d7hj{q(aaFS@l&euaPO}@OCdBXSGPNx&p4G((E&r=d$MXkmfrndJJ?x2X4L4EHw_{2D zpxVlP`dF!F&&@~lT2pu*VAAEG?#IQy0UjJIW3bz<7O)X3C*NG%`8U1BcZqMF-9iNpJ zStI1V77vgGtx*3;o#Ld8E5@DCQkAZZEI9}rRMCp&*18-((k&AUI;^C*ACfaDtY`QQ z%1h7H5OucG^$|YRshqs;W$g-_;W~~E^<#RT0O6{vKc2N5gr5yCxpDk>B8^!JIncoc zcwvxKRx>1eQz$lBTVoUL4E}0AwyRnsuV&7HuzUjUwjRZ@29TSa)G(dMSP=TGYkhgg zqASGx5j8Ugw?UH*TO;CJj@QSdWuEEvH>W$5{U6zsDLEdxyV34=&h8HDFZj04-|Rfm z{}W~+%*cX-`-Q2V$V39P6g;HAql#y<)Hz&SumLPZN!?%WJv|s{LL6Eqcv7Wu!WG^% z4Htq%6SCZpin(xZ=hf`6R-qjvigAa(DbiS6Ov?|m;jD1<%CZYxO&b{{ioG3|IR8N@ z30B=W3N*`Y@w@;u6|O5nKK)JL>D5gKlrzFro-#Kqr`|hc$Q+EH=?{|&%D`JNzszkn zyEQY05WvQ1CArPF`U%=#QPF z!47IBQHmQ@F?CKi{tb+6zQ`4w4``P$D0`+DXVUM9z8X&P^T`h#!B`KUF^T<`|G2zC zR8NouyQA1-SzAQ1dO>tKXO}%rX8r3`CpIciK1&P^;pGPXf9Bf1B5C04sUrg z6t3Q^PR$RyBjs(a^YY;a*iPR4TvT3_j;>jUYrfmsOynf!)X+G63y`q}YK;e+y^Fg& zWk5w9?HT$a7JNMN!^)ts@zQ+L_jZ+UJxia9YQbv6__}7W)b^7L)(?BFi7(k(VpcP3 zR}f)|27-n8i$csO4uM7v(^1fI#?)y`dSO-{GzW^(DSQU#R~!A3j2U@ z84t$7XTnbO)2?8`+x1usmD7fcJe^)dw4|I(x?8}VR&gSoGn5y5OZ_0Jtp{Rwy{d)Y z1P=T=`>Wde#)5TnWC@BCIR^%b==yap=o4mIYYFvuEvSO)>Ff562T9>;feL=14dEbuCGOznyGa&^5kj71^ zP9JtjKfU)tvO+Dp93ME&^ryA+q$RxgBG67lxaK0GVWp!n5mdW#Yj1mFxZLhk{M}fy z-3eu=eq!(}gfJNLL&WHn$|bLr3v;!3f}?e(VZ8B273P+{w{se&k3&0M{}L6g9Mf?R zePK=cHOBXE+@Iim9hpL<2yFSiCh@!Y;Z#|Jv4g;7R$`kocvFZ3_>lmQc> ztkY`2byUG}0aS&e@1?^Cbs(JmNe9#Evx%Ct40)FINSu{Yd%$H1JGy>*iPFPX5jyth zuOX11N{+GHgHgrf>TeV39MCZI%+=Uep_9P{fAeis&%5m4{X`ANF^brsc$>CI;r`%V zWn~m!V62z_d{|!TsMVsLMWV7^WpPY=>DOuLT4J!f@qH$aa0Qxe%YL_@k9|H*S2s2& zUy7dhw&_p&nPO8Yr1>eLu(n1+lYs)*A@yOQbf@U=Ipp`C0Nx$>azG^EYaI?bGJ~hX0#ORT>sH>oZxw_5V%%UMLT))=g#YveZBZgpO@szSjZ)@lAD`*6zLhNLcjOY-dgYFIo)aOWkOX-xrYZk(V40+ zh366mb0O4Oke~0gq~jsFdXre%MGwOBI-)h^Qz~wTC>MVR*T6Jp8SMhZ|H3QURh-8X|uhlIil$P0}HxF@<}@@+?y@zobJ>HaqoU?1syM| zbN6HDX5Z}-j}qp*FcoS=87Y_$XHFtQ>iyw^(*>~Tn5{$#i~S>!nW7&#WEDM4xwZT` z_~d@H&uQL&L4tPFu)M|-WvuH-#ryRHG*b%5HtBu+ENJkhOtvD6-s|;o#D$E&)m=h? z_%n6^?OU|O@L3`2htMT8823oXz^0roO*pKMP>Og&;v1g@8d!E>OA0rxIH%yedNU+- ze1|=ID?qsLRKH0oVb>8ZEVNFca~K3F{VX%=O6b^Y?n0a}Ja>#49n4!x%kQ$W-=p!t z09rx4d2^pN_~vonTwUBMX5mun&s@&V&!AQF>J;$a9kf~1>SgYwL$N-81O4(>Nn&@P9o5l_V+B!I`F4;(+^iZxLS%&zpPr-#>Pggj8Q?sbln7qnPscz%NVY@OVFWZBJpO62msF|+_#*&bZ?H9I+CbDl&Vl~e6d>$Ly8 z`y=qxZ4fV)uG?o0j=xz7j$hbqHuJleD!1|rUe52&7C&n0NaoIb0GW~P*s)>j6X~Bj zrO^Y(Y81o7*zel>ysWTKNLK{6&klDywrQzj%bpZpL}N)>K~>j<7p2DY8poOwN5mb= zI~5PTRvJ#46JyH5WPF?8Y@K_kF4+x}kFsUXF6>8Oac=E0JUO1%w?@G)3?Q^7jR*c} z^=BK6J%WDtP(dA9HqT33Tq->K&u%zaCxfJr>5n?b_`3({`H_?0IrHi(wn}Hg)6%uH z1|q|~D7r zgW4afi|uZpX!=t;(#!=x%SQ%Yjv%n7HoC>2eRb-tvS;xKu7&`m*U7g{`xpTkJG{+~ z=@)w;&^ogAGGv^Qtl2SXbV9}h^T6BAb(}j9QA$YgQC9XF-X?QlKA}nQY0%Hi-aBlj zXHX_``GeQ={w^k<)V1b?x&ck~(|X)*K_vB1WZaBjCeHB|x=Z_&+Zex-MsTaj&0VWn zdjP4y>yiyFaZ;eyF*A8<`E66ZMrd^O;3+%*sq6sOjFSF~9rH@TC;ynhKsil6bH&X5 zyIsaY@i48Z&NbHuuU)u!gT1nejXEi!u$`QmQHJv5ST80c)~JY_y|HN`+~k~#$9fGt zJu<_!KSFE$o!aC+#LGynovT}kLjH-?;sr>k)k(KU?|zKq4vJNiZPm@_c6cR5%; zHOUN9Cdf#}U()10-s2bt3oBQcM4^9V_iJYL6w~M2$cw^xO0YIAVHr>zZIxFgQjLo~ z=NP?avCi?~MgE$3Ijy&Dbkd@i0u8XXPw;VJV`$#0qi=OKO~eJbRA*m*2eIm{=4OiL z)DY|%UOQ1j+s?EV!~=yLupZGtfmgOXaqPi)#+>+N)l{uL0$Ot-lSxXXeYHF^f@}nH z@fRPF_-4(xmMxPU@}F%$!L3LGtzn4sieK&@-E3aE!(0LsSl8OZ2MEEjeFfp0ALa>P zF**=pHD8y7q714=FA*g=Gx7f({7r(`$c$*181gGIo|Y%=r6wG-rJ24Se%g@Q547=zV-Cp*A z2$O{IyR>BxzP@um%iX`aM3T@s=|P`BBYc|B4DBp^s12WLv4(ozIryqimFll@H#6J$ zbEZ8~9Qlq7JcTGGr5oYxD~eWrGEn?A_fEo95c3rNdfV@bsjgPou!|RfRW7tAwj7Y7 z3kE0bA_l1O)?uSB`?Fs5s<#TI3~Ybjnyo(i78)-w9OBKYI*a-o?=_Tl_)jnC(dwco zs92^4A~{`nt;k!-A@38))s=PUGf#KC?6p1-%5V}b-Y7Y1eTHigyg-bQEer_bdPiDm z3YV&=!!}ten0}=p`@BxZyapKP!Z_)?7rJb|+@t<^Gh*a^kWxK8VLG#g-+S{<;G6^mVm$PJH{gdq>^l z$3g?o{^9ty9%r1(VHte5xkHx?fW!>AAj@Sh>Cwn=-aGn3o-LG`4l7+|Uadp~Ix zVS|QD_VkfF5BKyNM*$snN0J@L!bYZ*;`R;3OcM9*X2b|N79H=u1q0o*jH{LkM6bE9 z`Da|;fBzo5kfDa@k{Ow{*`DXNRCu>JJB(lPVQLK1j{*0nlVSxFhx5xqcfLojG2Ar=cejKVj?stWg{8S73)h4YYWMaZ*`5V(#{FD zq64QjK0U8ob5YPJZ}ijAIGg&s9^L`DFNP0?Bg41Ho41bi-s}XHp41ttRXnJ-FiL$P zz2{bkn3VjpWV*|y_4qSQM`!LcC!MS_c~v-tZFwft0;sqvQ%fr{`Z772Gd~Vx5{mtH zI9Rr->2KKGr2VYuse0`n>Z4rI2@$`XsTc3>!Fl?;mPB&q-ZJ>v6z0aRg%%g-b@AzZ zm@IWe<|_=ROl+Pl?$>>$yo!U)cwTCVM91imYDa20h7uk({mcQwsf2g?ww`=f4NJQL zl!TM~ZGU%7)2~NAJVPXhP@=(ug@GPI1BR<%Z8y)<5Hqx!sf0LGW}pJEHE}V^gt1FK zCysuVk1m|;$CO|B-6wlsjY`=x7x>D){7VV#vSB>V9#3mPzZ9o1!16Q_4ze2m_IVgbHgat8b%@$j4;$pSV)HWP=E8Jz`Nl znUzoy5U#gpm0rLu@w><2pe+@R)5h_sOPlqDscr(3oT8r0>)Z>9aY{fR?@cEZ5`2*d+D{W%>ShJUkyG}AK zoq+(CN(e<$?6X7E>5nIW<-SgLK>3R6l1M}QxLug_{d1|y&t#Pcx{=2Nr51^?-)%Qw%g7o;(+n; z1`%vAzgVn@(U74}BjrZn$&iV@_(>CU$1uAbraj z7{PFZr}d3E-WxSU*mK27XDMqOnvSxYxX@F#lm~SVYQE2RW0GwS66SFxdtRPdR!>;+ z<8ve!r(wxxL(M69c`VvCVE|m1I`eCK?Q#_fvSv6$DS@km4U8%9RnGHVG0gR7qBEYP zW;C=l*n5Jwxpmi6ceT;BCL=SLyRM;KbY3Tg29x5>x#VCNA?+mGNud<<=2 zF{q06EZ<50m#L(0x@U)68Ur`A{OqNzu}$YYqBf~fc7QRyxu{cEp*SvB>Q_g{mu%ZJ zrk+%`+i`Ka$gxNih4ZujO8wIzVpr$^eWBg2%x%#Up+5Rg&Zu*xBjtnk;0X3y zmP9E}5WZC@#`M~r&cc1lEM#OrmOeP6VP1GnyrmjSps-&klY2AUJ~Agv%%h2AY^)-( z7_!vJcIl%$(rFC9sK)c!(w;RF{{4LW9>U0{RYN%)CKwoa6%rxaO}~TIj0rgM6QGqys(qF5H^!f2^a+V5{PA zIzN3rn54D;b8&fM)E4XNLuluCCBt-!eGSFoU!9CSrr;4Xm55-HD&gj8PmokNec+24 z`^Z-hoV~)Yl4HTj?d4IU&R}DZ#`lRVe%|E6Mz?MgZa{yWQuxgmV{Yo-9>K^`+X>Yi zQjp4HR51C7z2CfA_OhZ%V{*Q&--Ovp|F2<7cwEJq3FROn!}8@6j>x3P7;fc>dc__? z$E7we^OG0C;H;%q9(%`O2mZFEp*e&Aux{Hs3-ac_M1CD?I(W*)jL8auK6gWzG_FIu zhlQ0Y<*w`k9%(%Yjfno3KAMGM_4`zUh!pFKKBNrwu}*-Hc21I9;3 zyMiJ%mU$+^fT#Jn%m9(9M6I1Y^yH?|xp8OR+>dtH!t5SnpUg%d6%~cElEv63zN^=? zF0jwof0(JR;2~HJ=j!3m)FKhSg;qNJp8SU9j{lB=MDW~f`l5;eaQ|@`99iU+2mgJ; z$M6eHDfQ$+nUPTS^%R>ct7{-8y3y`+V-<(>-*(CQGU^N6FqUg;^?}|xWJzYb&zHQ0 zU08fpUhEPus^xH1eVnHT1lXuwzUXp|z+Lqy&exmVUR%l?4*m=AR#AcmYU9Fw(PTP# zx4%kW+8yY8_>d-#3Qx6=U|HA z2o=AEYq?}uTlN-~(X8!rwCc19=1bOJ+IX*dXfEc0OpB`eb1e;)HMZGDicvGxovb>T znA>HCL7V)EuwHf3_ct(l-zJ;;Ijn#Pb|gjA+vE>L)c=6{zSt@i;lr3EyJ%sEcr7AR z6~{7$#=NkVjMbOLPa*HJ*RF6UL_=qm_H*%a+A_FNuA2kAj~n@Up|%uUKO3#mFCwO8 z3Pel*7Py^(4T7KZrsjlTWjB-|A7TZZ{Fbiy0&eoexUM3*84! zV%Nsn#j^C|;v)_M5!wMV8(y9^KBff1qE${~*`b6~ zYc5WoiDR_+x_ib{$(RZXSRcUD<%i64)WZ2wo_&vH)ok%w>sh#?3wDgLcC2{5#eVRYN9cD~4AaU_>=HNsi}Y{Zk(xx9%+| zeY0~yD)oCtcOh>J_Q@IVB4{-a5|9g_{9*agIYL4Hnt`dRHwC+bMR+>Rd8AP7yUUP{ zkjJM}U{O7@7zaw_k*$$>E0D``PV?!Den1u?0&;TNX&GCbCJ|#Lhg4{b{^3Z|$GO`o zPHi2#hwg^)v^d?b$wL(Z#l))uqmqf+71q~kvT4B2+=rbk9;j8U?^_y?vlm7ESL>g? zv`RparE;1%OCd=V_##J&rZ~e$Yu-=e>d4+JF1(8ETlAZiuDQtkf-JH(f*O3RpmNPn z(VoJ!&{N|Qsl@RX1O4n11D&5FF#aA)n|=pmp3KAXRocq zLPJlQF{2{JLa(_vs{Y!*libPC*-kT#8SphlGM`un+P##B7#q{_PZx9psan-{GV8Ll zbnF;Hhwl=ozwU~z!L|$?wcy?YS(*rr34Ul*dK^7rB8IHq=ec%h=(V)4DdTHdYqQ?Y zD*3u9e6rMqB~I1ur&79?h}qg(Jpc&se_EJ)b2_=Sx$tF^u*T$ku@mFr&KmZero%u* zPc7ZFV%N$NNFgIJR7z9eP-ElDxGthUr1sd(n7Z)lCXLnWk=Si5WRN2t)@yHG?n^}` z6hE$k-`JW`)>h6&2UO!Y01syLYOS=GoJBFsY_j$&q9w9aqi3r37Odrx5L0Q<>d}H2 z1kylU9m$d-oi`)jU+IE3t^LG1-Q`(?6D|SB?pkw{-HqjAju+Gb7}+$s@LdgBMZ_id zXmnQV&k3QSe-EtcWQ$mqbUSnjUL{DU|L7c>Nf^Mtd@D_Za_9^Qk|(nvs08;y{M$V` zvxEz6@@(PYBZmY6C_Exdg7Lx}H*p{j|7%OXL`l2JUA~9YDXX#>UP_?ldJB^lwOLn5aGK z2if>Y=%Y9=2yfA=OqRzV@8G+$Vuq7s`WIRrT0PGTG%a(nf@^yU2~&Bsq4&UGy}7JN z%otd*@`D0?4#RM6wPs{hea__(_%5%7_l@ag5vJqlt-XaUz5g)K z9eKb9+idc#zLuFIW0;ni6_>HvEv*!T~w_ zZ2nAu!BBHs-n^b5A(hlI7|+Ge6j2*!i^XQYn&n!(J*wPEBIeAExw9zy#!m?b+>px8 zf5NvZQ!O*NGg5RHJY#C;%`yC)dK9*-LB%%rif~Z<<^V*-kI_MFP7p3l&5*z0& zY{DlDsd{L);Zw(Y!)y05N>EYSX@u{@PNqIFpz7YP(iK-Nsgwg+Ea|JoFiq{UI9sHbF*FnBU{DVg3-eY!giXri00=FeBLp zaUpQ)@$uAtC!*Qb?>T#AxHfpYB&#!i<7jVuHCD9!2@&uI-T#?qVQ<)*Vg8ITHK{D> zBgL?Qa1yCXCdtHX#zq)q7(n8yjG8oLyCs4w&n()3h6w@V!!Gzqc4v>HQarw0IzMV- zU44M#FvIgu>6^|(G&NHk-i~K?tL@2ziwjlm(hTekeN4>O>zZqcZ@$wKE7ap>N}i^k zS|(ty%$8=`bu(ia7hAR$g?>T@WZ6As9g($4M_ZNkUg?c@Rg>bV`XY%>SnoRS4~zOQ zG_5X}Tt1b`lMI);O;uf-kE?KHVc;dPP`sP;`$vF}%nnjkzKN|5t*2jSizV`Oo|Sq~bji0wR+{sgH$=++O!ds_y!=XI-r*!@G09r1ZV4 zkvV7r{=3dzLR0ICvP71!%Fn@u+qE-VsSgncX5;AQE%O3eHqDyJ>GvfvnRc`zQPWyz zj3qw;hs08K>Gs+EX*wd6*GW%!2=*-DV1GNVSGbybNHIKkjli*{S)vZeHIh!Nxc`bqK;uFPzvt=>>to*SAWwlXk(f<-dc`z+Hn4JD@4e0xc5aw-Cqo=k`Z=rum=SI5Pn-uvhV2{or-Zf{vQzFkP;jX~;V`fr0sQKD^}@?ycYTBl8?+pLk*CC3s3oz%3=a8VtUd2zc> zw$1fX^_8VDmVzFqQ&YkzFcM$ zP3(0XOxn5!Qt2v&eS&4i$S{3N(!fn|e!C zZAEUE2b+8q>mB5V`r594&KMH^agyx&aX-Faru`Td9^Y=VF_4^@Bah>@E`tzlK;8Mx z;Xh`qyS__w1+oGkat8|T#+@PDKM7U`B(EREZ0k~@pG*2t!z zp%=|ZNdLBq{KFKJd9Vsu<^{hU3u5v6t09joA7tIG=}~5+4M&Tvx5BrN-+#~M&-f_o zm8_&N&Xc%f2On!R67&ZR(A5gh=e}_=dY%;OlJ($f0lhSHto`m%T^i}|WC&Hm*F?Ic z{ulF_K05ScxU9KfzU#5E&iktG_Q*@f8lzMu)O&BZZV{S%B&A|rZj?8Gh6R(+IM;y# z+((MIB0IF!pItH=|GM3uCZ*vy)KqGZGrj&ec~kDH6?-wvrZAi|C8)nw@!@1N&z$3d zajxseai1A1os5m$-ovfA6B$a!zq!sOGi4$mR5ru{2N-oro)T@Q_6)O5gRh!yYbj6D zbkOd&LmqhAl5<^~IoOK1L)cwB9*sF3mNqtVpE3p4=~TY|-l$TV6A5NSNL9O**&!fC zziR~L+q|-%1dzuZMUs|YjjC4>{Ftm(lczdiE$>$6qJg>`_QnDEo zl&|qj{bzD}zNN)9A>R))f2iO@(`-!y36QpY1w}-mN1OC*4gHsSEn5ue<07{ zb?xh#QcRZxYXiHyFUjf2&c88GZZS~?n5x`Z)Q+h!d)$_TIC46;=1BI*Vk@`IEMoM~ zw1UimC%q_(dytKbm^xBAihwR$jlW-+CXP4%FcxEfrS7auB4o`;8b&2p72E3AQkwf| z6yM#a_+Ui>s60T~Hqov$n96zf6P(VAf$Ju<;~_%TSJgd_rVrj{XE$rFg7$OTal|D5Swz3c)DsiyVDT~NQl`n6tY_&JHYTHi4p;bJddmsReYx*xARfQ2E@%nWmo~D4CGP)B$nGjsvA}-D_WXD=ms3gcfTVbHbfC8 znd$WWBs_`1CGjl$XqNxtsw~#%qI6~e6d3bHNmY9zX?p6Dxf_OU%d!5`C$sIZYeWDG z!mBrqn346~^VSdfFI`i_fkMsYK%`;g3p2C+nA2yxkw@=WX$5O5u35>k*NDp6R&K(O z_-kT&msMk%fzR7@p|*m`=Sw|}!!g6~|l`yhqUf&}(Skx2fx;mcj2qU4I1m z%7;#|06V@(eviiCwms_l)qsTcw(Ee&%JlxDrim)UA!V!&|A@-+cz74SE*RNvRGVo* zZ}_clomkVGK_7`u3Kk2)*aScb3bT1mRuwN23)I{a52zdh;2 zUy2m~N}oE!fwevw+BU@ecy;9=2-sVGdejh}UPpxtp#B%pu z*78B;)KI}}&9T$0>(GemcHE((`P4h;_9!TPYXA4WxQUkWK7!Pi4&SQOa!69vrFv^^ z%U4q67MJ?BOjN>dscX{8V4TGFW`TIwVIfj{m-!S$lNjA22-QbiD;w#@!G@9|KA&7g5xEA!;t(3^F}@w0y=@bTx_hrxdPPO%gHnU o@?Yvo*}sP06tMoMf|G@%J>Wl3155(S%M=GIlyh2YrLsw}SURq95sVrr^iZnpDV&~iamG~; zMZ>br%XDTc*&luw37N}uGB?<=%w;aR;U$r5A&YKqGG!x-A;#_fTE$D;pS3^EIq&m4 z@AI7Z{o36nMV6Sz`H=t+W6d*{0!Z*y0ufTY3@cZ%cp-IprG)@nmIC;90Nllr|0aNs z0r;&3z<3B?UgLXb)?^`|sLi*S;UD_#IorDrE8(KGFgN@ci7aMG=J5Mp`T?W|t!7hs z>$Ts17;^tqlF+#(F{`_?M|u6_4a%7BqL1{SQbxs-b?=OcfsOHhywba)dg9k>MfLS3 zX|n(S@qbqF*UVY9@^~bMpS!C=9axZk;R(+I$#%b z_-OUcFBU6X=j_bl4lk`FEs~=bdewmexO(fd>fAHz> zErh7xV+(o_w4m_jsKH7?nOV~9!Ccuenw0QZ!9pl`JVF+a?8NOf54MSiRE!^r{8RxO z#6>*OKneHavxhVKvU#{OHKOXOxcg~~T$oq0&{9bZaLi==xp?J+m=9weFF(jN$%U}f z8#GkbJN&ikTujYTMC=ma<{3Z=|cn}w#xogb=`#Kfg^Mv zQ!_q!?SYBuu}FA3YIG40pXBz`0G;Wvu)z5@_F9_Z)X2AJGCDDRTKcghD=`+s`LEu( z5qVuvve}3I$6wfQzn>HRRMH2J<9DXrB_D*1Pf5Dw)+8t@$(7i1mrOHOrrLprELzO1 z5=}1ADf;Xj=rzpc`uzv69u=%_ak_nC1F(#~yL6-h%PLx4)nMa!&P7bXlH_Km*IDc0 zQbtw9dy$a%I3WizNV3?#32vK?+9AYvY`Xmd9{Lt_Y;q)7DhQS&_2n4JPwM_&ZI9-LUM z$H&`~t|d+)9-MHli)#+PC>WM=lTB?{Po=tC9%os{xj9~h95Y$(<#?}0AkP}x#=6gU z7obi_b*lxg$;a_^ZNTab8kYG=edz{b!3kLe>Q2hUkYRy`w|n?hQo%b#&Q9{3yuiX9;=jn(K4%&L8A{C5mxD6}<_)lL@&Je#R$ zkKA#qg@RFd`Q!lpQxr53o(PMQx zNHZsR>50>7SXQGYwVY^MAc^WkesGgz9+yoZkM@F7=CX-M3h)&RVJt>2!smMds|AS5^bRs5DJd3G2KTCy+F0SEw{HQer#o# zgb~Jw&Iqyv#}Z7$0Lezon9iEWhe2W-Y%{VUx-7UQQ?mpyNMPr*f}3-{cJGgK`aaL| zKJR(obNk`$?6lyZbwL0SoRO}}1)%Vjf)ZEN3h?vQoy}PzH%)xN|QN0mbiGE~%XlTuE7boNHewTCS+*M&cPWAtv z_s);*F;o{FmG()*5XsM72>{9s8bFXfAr&V=AozobgpBu+1Ns(0F$UU)4uciQ?iq!s z#E@0nnqu_x<@gu)jO*!IcBXLSv5;`h{$cFo z^ix~V)y=1d`Xqxt!-cV7iDOyKAoN!xEGaJ7ffB3 zxi${t{;Q5IdmOO>0+U7Yr!w=<^eY$N?joGRj+4^HFi+(-N}TrA!)cPXWw;-wop5Yz z5`9fQiEg?N4Fg|DjeeT8KM!5X!lK#m)8_59T5tbfXr}k4`<-+1?_O@##BIFVc!}!# zu|qoh@qF{HS8G1+d3-Rrx3iRxrB0HXk~rwSUC=_CgId_6UVVuqy+}WR9c*tVk;}1Se8Am;`~D%Y z+0B;Hs{a<$?WKRff|x}I0oUUcOg6_$G%BBolv1w48VS+pEIeC64gyX)?pU$i#hIgS zZ(efiFIP4 z+sU&PE|x2)0!E>dG4w#}D4C|WLuov5M|pzJFw4%F?OYrx;4A`bMwu3_%xt$kO3C;V z3s#b3S(xY>KP^)w(M@x+vfw9O>2CON9gY-ofidf<41lhALU! xNp756#xOEDDrN;!7`6Nu=6ffZXSbSo^n70Nw2Ud-=h}jC$k1o&I#Z1A{{?07-ZuaM literal 0 HcmV?d00001 diff --git a/luts/WXtoImg-EC.png b/luts/WXtoImg-EC.png new file mode 100644 index 0000000000000000000000000000000000000000..e0f0348efb5601cc1b09f60efc64a06f560133ce GIT binary patch literal 1632 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 a$>{OE4A{snSsr?U@Zcz|uyxO=Tk{unh6XqQ literal 0 HcmV?d00001 diff --git a/luts/WXtoImg-JF.png b/luts/WXtoImg-JF.png new file mode 100644 index 0000000000000000000000000000000000000000..7cfcebe49af8fff0ff46dd49ac3a3d1dfe8678ea GIT binary patch literal 1637 zcmbtUe@s(X6h4It3_)S5ky#}-fX*t^`}%`wo!UwZ6bdSB)lC;E^iiJcd#}E}qTsST zf6Yw|g1QBqB+ez6%}veJWU?(X9%`ul8*kuNYJLIrg z-D@t~?6+>YZc)9_Q$DTmOtICrNV(+3JIf{4xu1$Br+2A8pJbR*J3P<-%=o#S$NAd> z+0p-fNw+>I(X?AWsyV)VcieHsQTEm3D+&1BGfssX0+0sEQMlD9AF|?G%J;WRsnD!g zlTsqW(EWZ@FcFGSP#LF&enq4^F|rzz&}d1^koH^pplO11dhe&o4cTerV>X;peiv@j zM4nA)v{c4h8b7?X)`d*cs)=RmmL5w-L@9}o<3J&S1^#gybWB*WvAOGoW*;wI8)AsT zpt;Stt{*bDZ`L%aEoTkwmfY>uZ!Uqy(;D~m8nKddbES@7%Tn?miFIBc+v!4bpR&64 z%gT6JJ~9Skhyfsh)aYU2y3Pb*E<3F+7CVs@_1+H$&mE9GT)x{b&4!%YNl7Ce`ELZKlag0>$tozJ3N{H_ZSdYi;YN8=3)8y0JU!p3bC7i&p) zoE$Ew^>Q2?Kvtm8)H)`_R0U{FWZZ%s1d#b8U_KboRI!#8SZ@F^T#eurXrE>_KG;N% zQ=&+L_KFP8AveR)fQ_gm%po1tVMOW8I=$IwKt*Nld%JbycM6kbhR-X~=4zQn0|Dyu zFqMZ5ZUWcmn~nPE7@IL3PIzvY?j?k&BMKt%6B4~_@F|>`F_Fs- z`w-!%WR8sy=xVtU2=QcMReYH9O}>zMhvcIZwg>3CXro}7N#9vjPvW^U&(4uqR?{ra ziBM9AmW608#0%(YV|%6T`9KHZrOCWPfnF7+x$1hL^d>EZPf!2;D#=ABR3ymi$R)-E zojm8`xm@Jo7?Jj&Qik*Ucy@5U;|L*)m5VtEFt&cIHO;~;f3Qa%=@7C%mdm5GMiVaB zIi37##ypgHgQ$=R(7;i%v_Z~qZLPROa?i;n7kWb>WD5&13zX4p!1QAFTawNmNfU#D z2-x&^=Q!W2Gnw^9dFQW9IxOFkbQ$g<%MJvHeJEdGwIXWPwwK!0K9U+qB6oc-h8XC| z8%xqTQ7&I2t|s>mmMj%{2kS?5v=EYSoK8zoS{hdK1A%H|gR7UI9JcS02_< Ti&utckUTi-<+g+KE7$)CXlUw+ literal 0 HcmV?d00001 diff --git a/luts/WXtoImg-JJ.png b/luts/WXtoImg-JJ.png new file mode 100644 index 0000000000000000000000000000000000000000..f4dac331cc05f9c44b1d5af65507c24afe4f989f GIT binary patch literal 1801 zcmb_cZA?>V6h19b5MiK0MVapUfrBC4y)C79b=pD!%a;gD6rIx23$3=d<@T0|5G_s@ zab}poCOXO7vSBg;;@mQwC}=deFm;GSlp>IgsUrvx#V^FYZyRwY?%&!U=k0mU^PcBC z=k}JSro{Vt&+rBSU-dFo8h`*D1>hw_q0N6Og#wYYEG-#e^LzmNc7VqyWxoYrBLV)- z2B0eerdbaBykao|MB2o76+FY=gC`G_ql_o7PLB1wDRB3lM<>)&&;V}fYLzl$)AfO_ zlL>QI1UB~`*hif%D}6ItrE0xg)7ocP2nbaap?=sVj3&iiK5Rr`hF5<+6H>rWB{fjbBx z56*ivnNp2l5rM!C3LwBAMjhY~1=HYnre4q_f*>%$&IzdMYqy~zAu7+F@?OgXcOeKA zD}nuR@=Uv@=9s&1uOi#c?h8U>r^DmSbaeYg$aYgC!3{r@#%DVav4urNNSC~c^r=>` zAeYN=Xz9C*x^YD>{d{;ysGl)ddU{;(!$*Q+6St$@c!Z|e zfkrfg7aYLaabh~s?;J#6PbBIE3h0^a<;vK%&ar3JgzgDy8W^a>p})T-w8sZZ9l~g* z6S(h$hN(OCefREN!4?)3bq1V2=}t#QVP8A0U88VtI3hfdM?Tq0zWhQM2_1OwR5)?s zM76Uc5e|nv&IJ2^BeL+k&im}!nVFf@iaVlXjg4|dk6N-M(&LS7vnBRZgUKZh7#SH! zLZRWU%3Cn<-~mp(=I56z*sCDGD}CL%br`KXd>8|Zz2-jlu@n{>y#EySoU^DrEgYvs zXUmo?EDrhk8sz01w~%iug-1p|UWRu*?&|2;s!7F1vo_Y=4_kkHOWf*>PlxKcrvvMY zx7}9WTrUi4ZVfoqW{lj}IfC%4+WF<`tF}L)PBk|EWLlFjy*dBsK;u@+kL}flvEAQQ ze4kbOk%k<;BpmvresHw2JK{oDS65qGdT8J9yT3(T&>qouj`a3^HTdLF@!06-VxO4q z2W4etrcTyms?DiPIn>g3H^!~Md+eMxzo1~`+>Y|w(;PE5d3?UhZxH=2eE8T^ywb=U zc$rbR)f~ag!W8X z{wXS$_3wgch#*3EJ;RwaM#8}6avF}&g)SoN_Mpo7&PtBa@CGwWWEe~gkTN2{#u5@z zLK2jemPlz?1QDN{@o>Kcy-Oe%GjtlBp>r^e0aPdrC#i5UA_J96BWam5j3jB2d?MPp z;U$8}tTW^m{x@OQ8nPV`LdHArNRO4*m@Kd0M7f%Dt7C@|CjfI)29C+WgMihH9MhU@ ztS$2Jc_xWUx@MxFfW!xUgj_+hUFn4fmmZ@SgV;MUS-?+t;b6X z5YHd;;y5PX#;`esKuP6difj(M(uXjoL-8E$PNeg$hDl~tXJ!`=@397+(GiITHcw|Z z4MnSuB7z^jED065)=!G%3(3kc(+|)4Aab4y3#Y^~IT=$PhTiNh4aph{5oa(mz)}mv z7B)}0FYN}xUdLCaYOGd5W#e2DC>b3=GAm6j2%b9*BY_+br2NIs$w*ovr=>Ey^WvHH z_$ir$%hS=ajYcFNQkE-~1hsJY>Z+pWt;RtjqtN22xbthcAA;vOx#nE{1N7WM<|^K- zHsuio3}?kpoJ34fVhN#TcukOC;to{r++@1hsNskg`wAz;)XELEcMuNh*c4S=boRD? E0CHVZAOHXW literal 0 HcmV?d00001 diff --git a/luts/WXtoImg-MB.png b/luts/WXtoImg-MB.png new file mode 100644 index 0000000000000000000000000000000000000000..c4cc6d15646af8634ece154dffa6830988a612c5 GIT binary patch literal 1604 zcmbVMZBSHY6h6Cr?SKr+7-TTeYoY@J>%F_ItTzi5cfkcg0#wwoSlA0(+Itu8-bJvF zS)Fu>G5o+La;TgdrkQdGwZYJ=#WFK$oF+4qLAJ&whbG-JMv{`wyDMNb`lsC=_r2$N zo^zga&b#|kd0A0xbW$_`#M)L`Dgab?RDni~k8ibkEIvrx%8F8et&0G>F96)akoPKp zhXEL?0pPj-9&GMD{={+wB5O;EEHFjC2T#7!iIFJ9R%(sK#udM}p94^L*evF% z)=R&Ql(dhP&)e}$_Z7vd8SIEF*H6VeKxkOJJtcYH)wK44qu(c;ihD||>$xT-VXFWA zxxFT%=R@`3rs4kHd9D0LJYj2@~OSxDKG~ub+ z2ScGm=uh^(Z~i@AlLZd8&@^P3oXmrL_ zP3PF;KP^a~w{tMzi`bfD!vjb^^y?VXBk#;2*yP!VPj{yHOdT}m&B+nu9=tFXeI?TG zX?!azFcr4v(4Ws;kA}T5{`p;38@!+X=#SRGBVnf0-Bj`F(CLIe&_3iF-|E3)rZ>;g z=&|9iF^^y22GobYyz!TUNPx>Z`%%ox!5z%ZCA5;zYu>xPXzsH=qFBcgUruRnpeL`p z_=oE^cb|D%IM6SaWDFfK4UOJ{bXLDO>0ICfM9_$RX)UBQJ|~ zClRE2*)H>rj7OQ?J;+=XWXil<5nK|f5=0&tBPsSsM9=6M!WuZefivciqSC5~1A6=! z!a}NW*cG0uqcR?lXvk*RY$m4)>4v48(U8e79K%dTzS#61f#`Aw^=g87Lbi@UUIu+a<8#%N}W6MH0rceS19wFJ-k%c2CN}h$1-O! z&;N{Aa6&~gtxhVHP{CC$$>EZ+$Qnsdcn2vFqy~pe3>4Uoq9BeYTZP2X`WapBGN$Xc z_`!4?6gPyhY*uH?Wma@%;-6y3L#e%)6beorBzBpuS!yu9R&g1zxin?D-R&k8j~ueV z8o3;XUn>qFykG`KgR%ll?(EK)rJO#OGZ^X4pH1nbwP6_-tHAT7M?rZ-NnV_BV^)bffwSrxMb g`N2(Ax}0{I%Z2$lO literal 0 HcmV?d00001 diff --git a/luts/WXtoImg-MD.png b/luts/WXtoImg-MD.png new file mode 100644 index 0000000000000000000000000000000000000000..6a3dfc485ad198d178b6a6ff8ad17a2e32bae40a GIT binary patch literal 1683 zcmbVMe^3-<7=HJF<2*z!#jq~vYf=V6&TsFhTqPddfhQa)he~7W9oz!z?ClUXd`__opgt8KycS@Xgu+(q^$ME%w7R}(}XER zNi0Z8lmQ>xs#WVw!wE6gNun9 z67O+_`lE^OWSFDsSc*7Oj*1i(X{rFpy&woEVYJm~hj-wi=D&X5(oPNEI+ot5?f6B~ z$CXVw+H)$Yvj_YawkwWMB?}q#CFT5A-zIptFIxL(1^q{;p`5W@)O(9E+{hCC+~XnK zUfbj@YRE_?CXcUE-O+^QAtM}KcA!#y{Heni-cN5mN{w6JBwJP)^>I5Xqz~Vy<@9rscno%uIgCHQ7YLuiIqY=22479ref+MEnDpLB^Ij%|lz$R(z=+bMDn|5sImGf6qE}>m=}+VRt%R8xVTz1b zVg(+Ruzm)x3C#`hNRRax(FUvDU^N+0zN=(tyPiDNFilqYyb@zAmt_nPq9GI0nb=rD z=mxXZWXQnSit(SymKFaJ_yr$ZQTJfMh9&qc5u}YeNTi;iO+=@Wb$KFF9b)+MI-m^}9gV-rIDMI5kqPCCvO981QNu!k_X+%T zw2)^d#)k@6zS1Z7`*WNh62W*mSs@`t*N=6UEWB;R|Iu9^BCd>LnY7Mi!MR&9$U8P# zgUcI0c`U~OPiN@@e5LL6;)?{ED5sq74F-`tBt}c1O;#gj7Wq#S_{mY26evo7E%$bg z%~rj|YB0$=ZH<;5Z(nDQC-BsW zR;Z9xl6wb>OC`bSuS7MB7?f|EUPse9Jt|`)ua-(^Cj7`I-2&$o(Y<|TVjaC`MQ8@W N!RaWn@6Yix{talD`UwC4 literal 0 HcmV?d00001 diff --git a/palettes/WXtoImg-N15-HVC.png b/luts/WXtoImg-N15-HVC.png similarity index 70% rename from palettes/WXtoImg-N15-HVC.png rename to luts/WXtoImg-N15-HVC.png index be332897838764c88b0b50cf01bca5efb005a69d..5ed0892940a93ea77936692a74320430180d7218 100644 GIT binary patch delta 161 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 SV*#<`zNhQw&A%*eGXemWF))+> delta 3534 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 MwU^%PvKU1D7sTvbmjD0& diff --git a/palettes/WXtoImg-N19-HVC.png b/luts/WXtoImg-N19-HVC.png similarity index 71% rename from palettes/WXtoImg-N19-HVC.png rename to luts/WXtoImg-N19-HVC.png index e6465ac646010d10605abf5eadae933b05a7cb9b..a1e88963d608940cec44436aa1c337e3d4f4dc7f 100644 GIT binary patch delta 143 zcmZ2c*$_WLWul1-*Krv9^JM zm4U%;wvMdH^Q^@EuuFGE8(d>xV9+RWjVMV;EJ?L0$;?eHE=kNSU@$T;LepabG4j5r M>*vkqt!^>`0Ca#WkpKVy delta 3579 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 literal 0 HcmV?d00001 diff --git a/luts/WXtoImg-TA.png b/luts/WXtoImg-TA.png new file mode 100644 index 0000000000000000000000000000000000000000..dfda1958508e66e82776d0744ce133fa0418c5dd GIT binary patch literal 1553 zcmbVMZERCz6h61>O0y5>;)uivyvZ1@87ud;>$+8g?daG#wlTMfbK|4kdb?fL-j>_j zxlUXP;s=C?5f;ZlG5%mNLl~1N8yITTU;-qYB0+{)vyq_&vjj0iL_BZnJ~a5p_D65e z^E~Hy-gDk|S5;+6YSJS~0FY`gwN(Q!_{e}-fgfLKp5*X@G?i9Y0CX$|2=4^=9ZTUG z06`YuCkKGJ8{lF8p`oXqKtR>Fw!{Ya>HD3Jd-q{wo@}ouo;SfLQ$9Z4JW}qqsOw8htv_CLe`!A&DM{RXK|2=xqqVDlaOvp#KU^D4#XkS@ zHTBr_&a(#vzE4X)1E-jX3NpZhkL^lD1}H%ROcKN9OVvQ{tVn;1fx$;N^ielCq#k z^sJsGoWZO&n2mX)q@s4JM~}ZL$f63DQ|8T0RK^1m4Y@3r%jVT0-H>lK8gf|H%(8b? zuWtE=!0U4f?$Cb=b~mt>QII|3AY-0@?DYB{pecx(d~Ywlg_?A#u?Z62L=OT1A5N^% z7ZhDtSC`yIJT~EC58oC$D3}WP+gn3eUqN-nBF?gj_wu3)wN}y);KhJXA`cqdM#a(7 z3s9$_x-}BNHOPxiA>a%q9mk%_xpW<|*n~P~9pxM8_Zbos;;8j4StEFO5V=B~ zUu?D>sQwPIhiJG9CjIfw&1LA_fBO&h5iOVUuDjmY1w_in1-V^a9o1+j@J8fNU#H z4KoCm!7?;3Nr+>Mn_-W{#5wG@eb_?{Zc8MLW}Cq{m2OIKW0^bW77!Esvi8I6`91#6 zbNg0RWl3E0hG+nYGw(E70A%E+45H-ZF`lrEArDl)(^3J@wgtd{6yR?H`ELUFFu<)k z0DV6|if7=$i-m-Uw3n3_;UWDFeKpcckTs&Y!n9^u77@2uUwXDr47Y!LGKA}&u8~<*i{AdEhNtW{&+s>vk1|e zvojaI&%VM#vj2lsolztH)er;8upv57iy|PcBOv!f5d@zGU3?(LRs*9DTpPO#J|7!6 zP*o`JC_$ru!1Gk(AtI@h1>}KAc_=xmr>IUw>}zB1N}#_CM?nP0K}Mq@crXU->$+Cc zNPx+T%i5a=A?AtmKPHEx5W?593Cn9Quf=x zp!`l`LSm(iHZS#2LkC0h_$SO~lBnj|zCrnSe+~~lHjEWY9W%|GHokiMmsN9@{IBPw z%dXs)fBgHcB<1|=o=$e}`yoh{nz|d zUiYtsm(L^>gnCoTGV(G)<8xMWUt&d5jc9O*PSNLJL94_+H1xeo@E8eib8hmr9K|1unHqatyih_S`8|xus%4YA}z zV}S_O3d|_5#!BSst$M9G2V*_P^O2qVA2V=nhqIye$%YfP_(x*MTFM}jdc2~I^Q_>> z3n!T}o9+=$67?9J0$WcT0k4}(%Y;v*9Qlp^DV`mSx5T4E*30r_H#P*S7+r)+rhMbQ6pNn4Nog(W%Wlp});pRfc=Ff>Co(3x?!tnaBQkReW|K+||%tOqLVNSs) zwK}}LH;4Sig=?s=dC(50iv^y^S9Wb_H)5*V#sgW28OH-yeu zqDg^*2w3-M=Xk4LrPHgmbm#3GN9ip|r1EOAY?q6a50$$N2E^od?HxS4m}-ec)>cng zarawAJfSyBUU!3dh}=8aQzN>~Tq9~`1uwmEDkZ}xRmjeYwr6B9$B16qWVPF66VRi5 WDX@~+z2Em7;lXUGG@dK2JMteAAHFI8 literal 0 HcmV?d00001 diff --git a/palettes/WXtoImg-class.png b/luts/WXtoImg-class.png similarity index 63% rename from palettes/WXtoImg-class.png rename to luts/WXtoImg-class.png index f247df78165c5c599e548530b8025ecd5386753a..3671566dbecd769a726b97455c5a20daed3e277c 100644 GIT binary patch delta 161 zcmdmyJt1&{%0v?vuIG#_yegKL7aeWb*u<2sLV{HQi zD+7byY#mt)3=FCzt`Q|Ei6yC4x%nxXX_X8{21eL5bVVCn18UHKYbeRgO)V}-%q_s8 R#~fmbCZnm;=DWtX83DU=F75yT delta 3561 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 diff --git a/luts/WXtoImg-fire.png b/luts/WXtoImg-fire.png new file mode 100644 index 0000000000000000000000000000000000000000..35b997aab49c7e1958db5dbea6f859f3f7037454 GIT binary patch literal 1565 zcmZ`(e@q)?7=C*cb`~o&z)W16U)+W)K>ekY3AfVV@!ZV;~zR^4eFNQkHo2om`q|^M2(6;qH#ujzq`^xqP?Vd_df6Q zKJWXyUwgWxxxsF`-39>m;GRG$fCV2dz;k%J>bPIP8|mBA+5|9K0+4(R;7<%CmjDtJ z;D;Un@f^TyvGZ>|unU2_-hB-L_=jDSv#*@N$R<75RKMvvi`Bke-22*D5r7*H2K?>A z3qLK)KQ%`k0sqm`rOChC@sj&m?L%-E=;fB(rDm}!w$!vQQ?n6{<-O1O2I1P;BE&SmWPW>4A3(>x@rXuRCI|L zkTDBFs5WdJXD;FkgNm*Xw6I}WdM3z&?bqpa(G1YyP704t8^ffzjJ3)ct1$~a(_`ux zi(_0fwrpVX9`W569 zF-}0%q&_wX#8q^xS4}A49Sd7m5jPyJk4Qttpx~{RR*#He+`)MD3VPWmMI}Xteys69 z`L^*jY_}#INk~fH2ncSkQ=pg2KD&;X;X;GP6iAzH7Hw3Ou&R`k1B$FmVX{wF2EuA| zxiufg*h?GH&D0-An{btIEr`|sBD(6P!N={~1p3}+vd6igpOQ+8LvT4ItDg5ZL$#A6C+dek; zz=OLX^_8XA@9SMtJzu{)yAxCaqOsxrfbz~XYUd;qUA3Yg!guUVpFC z!o$;!5b?ciR?cS_@DZvVL_%yCX@XCQj}k8Pbj7Y)FxYVhQI5A?I!m1SadNC&)6zi9 z`*0)9alze%a@=kdqQ$8BDV0rg5-iV1Xe|m>z~bSk@8pXzf;~eai(K(bVSDrf-uux^=-&lxk@K4kz zX1TcX8>D@2#kg#G>_}gKc^O$YxnmO|sGX&cK5?`PyT_XGUKZ5g3?^l81derO^(C2t z^`l)Xx#IMx6M+0wHl!(gW!E*!uOyp-@i+D$$;jyBN~{^qOHw6R8baTqI{D>8N~;%s z*niMEBi6YDI*NjackW$c3O*eSS!|U=z?j-A%goxe0U6Z3a|uh@#&_9cN4y^S?kH2; z4yDZtPX-LCUfF~9)yT`a{rGu)D4szk&V{=^-rZ&C6e}{%5tchMisyYE_j*HrwMzGQ z)Wo)f&~)>{uZT2D%3d#7-4ZAHT`U;1)Qy_9!zey89N*4){8Amzlwn`V%{mm0!{OC+ z49~M}(h4-0TF?2oDmS}yB(A6x5f=WNunN-2RH=>?J&c>=_!^NBS=Z8)RCfzvri7Uy z%SfESlMc?q0%aiA`gu}E=_r!cn{|4#A)U-Aa6CGuLth~zV}*;6SaUU&v4DvBG>T56 z(jADdUtu=rH56s0s3+4t*!&-XM{sd9fmaLmZlnf~AoWEB3Hk9!jK}*w8dIdmus!=e z(#*pe8z-{Wcq8BwP{*nSKkrH&T=)>-;SOiJ+4}HC!DwP1ZV90HY^=-XQ7@}m56es7 zW0}NyE&9cP3PWo|c8j0os{=smjar(zqPhM%!op3+5pe;sREp%x7kHPzFC*9SoW#1w zJdUq*37&~n_R#Y>`G^>qFr_tSP)DacXP?(W;@Sw7Mr#d5YV{!v`t3zL6fjTS*k3W@^N}{c7_mSb%;^+c=J^T1NiSezP?Uf&z8ovH!mKlz^#(lF z#>CV3Ak4w^QZ!Gu8`+1#BCD08Gxk-SXn0=g1xPvq-Uws-TH0&`FAi&k8fhy!T2Q`T z66~H@vYr)v_)zJzG_BQ9%dzEwd EKk_-{PXGV_ literal 0 HcmV?d00001 diff --git a/palettes/N19-HRPT-Falsecolor.png b/palettes/N19-HRPT-Falsecolor.png deleted file mode 100644 index df1c1fd1b7b743f6c353c768c1d0fe18de416028..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15849 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 NXy1)wZLZpu#@t_cBaH#q~adkFx%eE@uf zB;F7JQ6vCf?F7K`BmnB+r*Cewh5*)oy`w!4BcHsV3x0$Y9LLFd4Xz(6JxBAql7%g9 z07yM?vZs2)@`X>x>5De1r94cerRgqTz9iJ&pI~^;pb~!~I4mrv4~rpYF1MPO;*`>e z4Vr4%YKK#|8II4V?MXnk{0&3Yb!=1mGNr=e%lFmpDb!*KmOPh&0NE8%NkpY?uoKN& zPm;V8fEnO=0G(t39%BaZ00V}>5hY$j-X~512$#XefKpT~7H9$+1K^qzj*-%Ye1>=W z6rpf8=u?rG0w^A^0|cN1Rm{`?uF4?=@P#-o0@m1Hun14BHVA`EP&tA-5=v2apl{Z) zR+7X-*sLuSkEnzPUub_4K@3%5l7D9dF^oY>*0z9GH_fCGD|oP1N@N2X@4p_UKtk$c zFgg8H?cNj6^|}KFC9msswp10Wx|iinIIct-Q(bb|;wggweZX`>`aA!y8bJ zgjD(3n50z2ydL2xDE21W3UUD3L;Kg0E@1l*q;GkLhpTgUi6kC&x>|U z@OqT5mb#gFBL^{lLXf6dyMAQxyNs*Kp96&?OjI>xZTXO^vSAFnBG=`jh@~Nqb#9*; zcl>qp=B&4`rH41ITb9r0xmi3pJ@b5ITFMk&%&?lb@%cFpsiO zYa!*o9^zV{j0Y?fbaoMtQbwVbQR2H`0ojI11Fs8B*8g_gpg zrKK?#h$cWBV3eg*7Luu0)s23*MGgM0;?*3BWaIf)xTl=Gq&aUo%@Sh2u z_rDkz9D4a`r=>2yM@|uS;9m7guax7oUxP%Z=xE?M7waEVn+@xv@N*;(0j&;bS-6@ZwR-! zanw-xEPi>PaO?5uLKnqHsC-;#`wijNBhf`xiINqR2)e^B&lB2dghvXJ}IwL`LWBl;n!InzU+lMx;QqnRdp=)VCRw{_2%EbqQT0 zs#$-wP7GGZ$gCD|IE4 zg?3u%nL@`*;Wi(~3LVD^5Hz9lC{Cv%zdwFsf4m!(^)29zsYF!;6|>h&JKGpBDB6A^ zQ|R*&SEr|5k=AsCc5K1zmAdGp@j@}sIfa{!{Bgq3XO|cl*vgKwQag|As5@uyi(p%l z{66F2$h%yxY@ePwMl5mfOQw5>)uWxkZM-GP6?7{9+D;hj=sX3h#EyI%b?fzM7{a|g zlcGt_)7piU-U|91KF=gixGj3RkfPX8z?xmTMPKQ*@;s(}3GJc?_a`lvszJQ&-%*`KtbcTzV`(dgdT z`FmqUMnrxFk*xICbeDd3@NzUy5iKa0+8wfB^jFsx+HrYeuyx$)GI#CdoXm3xH9}46Y_kc?$kUEp zrUNm10w%Xis-JzqSn#^+mhFDvDyj9)l?&qv1AdJ$38N_u=-y*RE375R@-LNn_zdJWn{_XgYE60-s)vtu+rtAt4@P`Aft8e;O3`L7 zCfjPz^u3mLVe^!8@U!y*&Bovk{q{}0RXc+|+&SJjSf?sd#A@Z-}THWz|+qJ#@xU0bPSfI{()L`%2JTegId0YO3VLcne6zyIcB-(7zt(MT4ea!JS9&Dv z#!kZ%>Fpial16W*;fD0~j%-Pzm#n@v^@2ZS%`Lj*ESIj27F-!>b!pSAoQWca^n0~J zve^h(R63&2cXs39C}%OyUt-p{=h^&vOBif1_!RA8*X;+QZ$u-7u6J4VcfEtT_vg6O z{I=3Rv~-Se{UJs3RxIvYqKVuG|29cJQE{{jIgJOOlqL)kTL)kIl=a7Z>NnILB+698 zdb+ucl5KG{p`h>f68_^IJwtjd`J<)jt5z*rG4F@!`@7eVg zdi{FqHkk{lCof>m&uvlu=#|}UeM4PyVD=JTy5KPI{%iWYO>8HlX6_)BMjRNneVP`I zBc5gmM@#bhkd6~C1~Lt><-{Mnp;5-w-xJKXuKVE7NAA)dLYIh|Quhu zi3tm~9qy{0(CMi42|a^K9}XEd>Q^nyBur`*DD)mFN8RPFX}|c$uk}2yUbb$bRgd=h z63Xqe6Ry=tO>Wm)dx?Qw=eQnXkg)?jQ`IZZO0#WbKP^-Bt?u> zib2AF@(+$H8bUuxDRvAbF74#%sizs~55_rM_6^NgoLN2nG_f*Iv93;`e7mTjfO=N2 zyX~k!oAJ)K@traI-yJ+VK4)g%XzPTmu|8oUG&2F?M3uyJoOQ{125!jgjylW_iwv|M2==`2x6)+*_azbcqhF=(g&J1Gvv1x(JtVqR2Ams-Z4neuLoC?S*|Y#;Qy`KBz4vEDF$2Hn z>x@EN60J31v}nnuf%yvBXha;O>mzb&m@qB@GzN{y0c#xy-jOtBB#TY>S0UJu|Jbht z$|)go4s2RP6pa}W2gt_ehGbHM-lG=~lX&aMMywOuIkN^gu$X}?raoaalg^<964uk1 z!GSD>XqD47C@=?6wgEEErjlf6Zb3RZ!zll32MPzzVg_VGQ*+YlANAmGPqTY)_6sLC z(8FkeNwzQyX9iQVH$8=zDx%WOFEWx~AH|;C0@>8kghcaXJb*Cq3k)~N<^YoU*Zh&p zkc1eUBKhNLmm(QgM%dk9;=;n<_JNBll}aF69NL;2|Jl?Rkl+y)KFcxOWH1N9izTwG zAWjUdA>g%z!*XH-6QXJCNThlwhGeoKh2T%)_-Uf#_CtP2Cf!+Ker&?md>N1o$)2H6 QMi2*1Yh3J$SMA*UPavl(0RR91 diff --git a/palettes/WXtoImg-JF.png b/palettes/WXtoImg-JF.png deleted file mode 100644 index 1ebefab70c050ea21e49e3c1b16b5fb7a1747251..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5005 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 diff --git a/palettes/WXtoImg-JJ.png b/palettes/WXtoImg-JJ.png deleted file mode 100644 index fb46ca872fe91453b9f30dffb6ea44d6dd77900b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5169 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 diff --git a/palettes/WXtoImg-MB.png b/palettes/WXtoImg-MB.png deleted file mode 100644 index 07eeb50cd685fd17e7d34da30fa5a83d46546c62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4972 zcmcIo2UwHW8a_#YgbgBs%wVjbtR(EA5M&f7!v(mQ2@oU+BmqH&h>D5>aa9x)uTreY zQf5WLLO~P?BCDYaMusaks3^$DJ%4bteQuxU-g`?;D7``15_$-1bEQ55u_-)a=}B%ALlGLpt?40LCJzF z)Wq`7W=hZtM%cEq6#6Ius^lQe`gc(f^^)pm$0l8P=Nt#0?@@0F2-i0qF<^2}=F|21RIKW8!ZFAiTV`zmBd!8QtZ} z#N*PPZ(>};iGE5e=-9KVnK)>!o_$V-cIWjZ7ixsI<}J0dhr~7b#LHYs=9UhZi(zt& zudpPlX}!5;u`u5uXVoviy~}LAKtEl{@K}2Hs`cH0_rQQeUZ&A5?1PVlBhBAE%-ww% zq!}3s@Padx1bjl^fM2K&a~52pQ5b-McP_jzaE{nWA=mHbsvqWv9SS`cXFHsuKg^rt z-~tYu_#^5Q4FaWAuI{dIBV<4LCF%z&0ZvK^Dn1g}<#Yjj>VR6zjve2MH31JF@mcU3# zN=QI$GSmYJWyyJZB#M;E>R{}mnEB+LS?6&Cr?OkBYwigbn}x8`q-7SUsV~&{YKgwV z(q-lrmR8m_wkw@oT;0BQU$u7KdS5^P4I4wLw6O39dL)M%8@DY!Vf(J#d(!vr+n;gt z*zpr5zdeHEu9ua#F+R#n&h@MA;cPfg9YTkdq;@9OU9?fZrIi2rzK zcx3d+(`RF^UcVWC`)*?LJ%S6t`Ekr4`x7o@hzl(tfsw!>xKQYA2ykTyNj;L(Jj!Zp zaE!_#@=n}*r>yg3x1OI~q^bw(mQAKlDmN$yA}E zmRgp;ElaR5z^y{ZtpbEi>U1UObmtExt{zJCkz##`a?5mziu^pw=S)Y3Z6Z*#`P(c( zz*B64p<2cM)+4mzx;K{_V0KOvih#~(>2Yrx^^u#qA??lkYf0aE^JUw-Jg%>1myFnX*o^T~ArP5W%iNli|UTd&2) zwjH5U{!?n_h8eog%dR`@0p8-X{_*5OULoPv7*mib4|d7J@WB$;-O3PqsfY;9;$8FTF9r5JdK>helx z!g{tz+||Rmeh!VZXJ;e!{T}*G*DBw3-BQt10-%;bq~Eh6i%*D0<=KC9JP|2yFUi`uvLc9!8z&D1}BCdFk+Qe>9em z!w`&@-|E?>z<3ehaOoN_XzxIU^ z^3Aeuy=#?PeJVQ!2vooGN$W%)a|>p+re*pOT54%}6N4`fZbdRLy_aqsr?Kd?VO>n&&7@4^^t9IsLN_Cz>LxX(zW?^#yKY2xe6NKVeDWwjk?s1SbPLpE)iR@X~A4Ni-})HXV3uA z0>3Jji6;}uL_EpNmTYEgVUBn6TsLu=41dx9Um+gUU@pxz6mg>g#L$>%X-u?OM>4l1 zTiKFq42dLLB2kcaeeWL}7%VD1Ea9&ma#}HV&|yBZgEO5?3q_Iw92P_$!ir^5zvAhP zL%od=x|DPz>qNDO)EA&%B z{9M_zm{=M!Gy#yztV~G6X2Y&ws1u`gV@^ReP6Y|+6U^b@onzT^DUd8|&51N$#$Bi;d{V;%*<3)h`dmMvB~lPG z3#5KY+NDUvl@ar`uy9dPFn!?VO`+gPHU~H4CVt%NlM=r!A$rc@Wvj6wR4*3$vckCA z;28pb{#=$jBOD(`V{?$xLpC9iOvv~U8aG%ICA$aOiz~U76&1|Jf3BA?(S+n18EXpl Nz}>~mx!5Tv?cb-Q{xkpp diff --git a/palettes/WXtoImg-MD.png b/palettes/WXtoImg-MD.png deleted file mode 100644 index f350074d69f69f1104e7d78403f232611772e7ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5051 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 diff --git a/palettes/WXtoImg-NO.png b/palettes/WXtoImg-NO.png deleted file mode 100644 index 44febbc2c367da8b8271d3638b9f8ff65c4ff931..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 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 diff --git a/palettes/WXtoImg-TA.png b/palettes/WXtoImg-TA.png deleted file mode 100644 index adcec9b5a3108ee5dd9024c85f840456695ebef2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4921 zcmcIo3pmv28vp)g821QExlK)lb(4E9c|>=Z91bOa+&k}N4MSQ?DL$no&Cmt=KJ3J?|t8SKhLDPIy=ZnE|mlT z$T)7Wa{~Z@TLg%Uz>oWKHYE6g4W&3y04Ph7nhg+zXME5GHzxr0tO9^@0DyOp#CZZh z3=x25{s35>1wc9S{Eu6#Aww*1lY<=)ps(DY^1p=?9NW>!9ycr|x=3}2&4tce0EqZF z+EF~>xV+!)eP3X!cpXL=gXHN^(Y(4qP!<&poEetTYs1s0ZNg2d1;hGP*J-z zATN%ZGy}aLavIc#{VXPAV8IFhNN5+@Foi7CMLj{T7h2+rV@<)1l^nODFuo1Ll^rM% zb^SiF50QKb)Ek)+Af?D>WKJ@MuMRPNROpBYzA1r16ay&O4*?0C?c|r_gpJH4XNgp& z+1|Z=R-_t{Zt#~J7UR#%ZAKi3e{lS!@jL(Pn7B1LS8N$z$ow1>KR-s3G~ZA86eefQ zv<~)Hq{i3K9dE{I9twcg2J+=l8M!I(VSTxKNSC+MzK38k$xJjbgLZB+f@fi>5a4E3 zKlYj=%7XE8f_dh$sCxVbRPqMh2$H9g>rl7Pe?;Au_iWy`Em;aHcN5vDbLF$f*wLZ+ zqaP{LF40y6_O~r{n`4Nhy(_6T^v#isNuN z2?=pYX+;@nDJkhCixn0rsxQ^lP+zL1hF@Wzg2n4$vh{z)tc|_0%G+~^gh;S`HL4jhh zqGC942}vm^P$7qA3L}EWh>BvdkedYg04p!Lco~Tzrm!Udw>(Obyf6KtIALwsEhV>m z{1rxlto;&_Un#3BS-NtSmbT8<#wMm_<`$Og?CjS&Y;fG@?y=R=Yuk43pkQi9Xc#R# znjI6nJ1%}t%7KH24j)N9dg|NL8E3vbn|baZmoDdB$-nyjwd+5WSKO$qs{Zkx4UIoH zHQ#Qz(|NzEyXQgguiVFjL&GDZV^5ww8-Mls&BWx}sp)qpE)?g7wSepoxa1)&5iAyi z#i6(mk=-b8d93I%lGtL(7F#7`C#Vr}i(8({ zvt1e)TcmB}8G+govZlwHo(3lCwFYNm1z^`P$-uzq^b=8OJ++5YD&p%>Yn18HgQ{)R z3x+0sZJ_iQuk_igm?D1Nrkn-=Si5IBW$?65?W$de=d%pwITdf-9`XyTew^l=uBc9p9z%e4pYPbd@VoTTfC?Yu2vT#_XFa5CDyHxVh+WryYEE34p$h zjqi}!lhq;c; zm9a|h&cji*-8BHazWOp)y)~0H(b;wT5)XWl*ZkdDG1;tdddj z)`GgoVdW zxc;qy#LfLP+PSiZwSs^Ta;YH^L%N8NrnwYgH|^)Cz32o)Mt8rdnvbZThhHsidyO;nd#Lgm-93}UaJ$o8^noSzuc&_7UlbYgL&OIB}!Bp9Ck|7H6R zMNBRK#`=_x2S=f6aZPeX$#8;~tFfWjba?Q%?D6YCqn;3{%f6@RN2gJm^l@ax{HbrV>)@o%E$kDH8{4Ix=xp!K6b^dZrLO2~@6Hqsdg1BQP|yEe)m&psauR8J zn8Cs)9nPJqRr4`~uwkzbNH!fO@k_^L26DE16XPTRdaF!Z_C8+HWC@ck0PnM1?6Tu< zY-KFk=(^YV|I|H}eP@w#-M>}`hL)!Jih%HzuBXIjiN^Z>33N zgpRSNzGcG+o_fvo#|RP?ah|Tu6C@j4T{sx1UB!L4v+s#6Q~Fft(Y0%J%vDa*+}Rah zRBT;67}vjqJA_kunxjyxb>l{dx5VWmXD?$fDYeVL^U7?qu2fbXUATm`G`|5pf6ts> z6W2qpQyQaC38OD<9;HU&2s$>bmiWwD=)5TVPK|^qklxmMR39sPu0uZxKu0+6;7Ghf{@d#S`&0M67M z1b%-{?ro;-^(MM!+wY5-tP+5vQTY=N=FQ>nL}a^1leKyhb(K>M^~U1YU-Ju3TajKf z_b9n4SGJ)+rhEs#IiHd`)zDW1e?Z=38n%Q17df`=?qAh64Lk3=rQatKq8tPck2s-WF?fm zE-Zk-phn;+fP~kMrA5;MBd9t9N-n3MfX3$nYk@Idn;k}F(E}pzG)4$3fJF_~u_6|q zgp#X-k}PTfo5p0|J!o_)Ae!Je#xU??BAJLM8CjBzEKQ8@4o)6Z=g9E)39u5?2o7LV zErU=sDnJGGiKhBQ6AzNHCE3i9Y@$mfSrUntYkTB=r$A>0(?a6^T%mi2Q3w?jQ4e-B z7BvVB3Pdv@`aot3BX}iOV*>Jp*|LwI#tMT5X6Dq1sCY=%L-p(#FfTz=I+ei&9u&M! zG?fv}Wa0l)3U-PV-dqIr-YmyScDqe3S~R3o$qaMB>-eBB+2tvM`8bgierKANc&LS1GEdpGPM;I)m-bfkykW2vlYw0p<~B$5FcA4p{fs3KAap}eq? f?#ze)7XDMc^oa%}&+r&S$On%0&UVFX{rCSHz3S?y diff --git a/palettes/WXtoImg-ZA.png b/palettes/WXtoImg-ZA.png deleted file mode 100644 index f385fa4942f1f6f4fe56c3005969df8456cdeecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4922 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

-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 diff --git a/palettes/WXtoImg-sea.png b/palettes/WXtoImg-sea.png deleted file mode 100644 index d92837a0128b19ef95bf3445cfd44110bc813b94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4942 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. - * - */ - -#ifndef APT_H -#define APT_H - -#ifdef __cplusplus -extern "C" { -#endif - -#if defined(__GNUC__) && (__GNUC__ >= 4) -#define APT_API __attribute__((visibility("default"))) -#elif defined(_MSC_VER) -#ifdef APT_API_EXPORT -#define APT_API __declspec(dllexport) -#elif APT_API_STATIC -#define APT_API -#else if -#define APT_API __declspec(dllimport) -#endif -#else -#define APT_API -#endif - -// Maximum height of an APT image in number of rows -#define APT_MAX_HEIGHT 3000 -// Width in pixels of sync -#define APT_SYNC_WIDTH 39 -// Width in pixels of space -#define APT_SPC_WIDTH 47 -// Width in pixels of telemetry -#define APT_TELE_WIDTH 45 -// Width in pixels of a single channel image -#define APT_CH_WIDTH 909 -#define APT_FRAME_LEN 128 -#define APT_CH_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_CH_WIDTH + APT_TELE_WIDTH) -// Width in pixels of full frame, including sync, space, images and telemetry -#define APT_IMG_WIDTH 2080 -// Offset in pixels to channel A -#define APT_CHA_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH) -// Offset in pixels to channel B -#define APT_CHB_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_CH_WIDTH + APT_TELE_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH) -#define APT_TOTAL_TELE (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_TELE_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_TELE_WIDTH) - -// Number of rows required for apt_calibrate -#define APT_CALIBRATION_ROWS 192 -// Channel ID returned by apt_calibrate -// NOAA-15: https://nssdc.gsfc.nasa.gov/nmc/experiment/display.action?id=1998-030A-01 -// Channel 1: visible (0.58-0.68 um) -// Channel 2: near-IR (0.725-1.0 um) -// Channel 3A: near-IR (1.58-1.64 um) -// Channel 3B: mid-infrared (3.55-3.93 um) -// Channel 4: thermal-infrared (10.3-11.3 um) -// Channel 5: thermal-infrared (11.5-12.5 um) -typedef enum apt_channel { - APT_CHANNEL_UNKNOWN, - APT_CHANNEL_1, - APT_CHANNEL_2, - APT_CHANNEL_3A, - APT_CHANNEL_4, - APT_CHANNEL_5, - APT_CHANNEL_3B -} apt_channel_t; - -// Width in elements of apt_image_t.prow arrays -#define APT_PROW_WIDTH 2150 - -// apt_getpixelrow callback function to get audio samples. -// context is the same as passed to apt_getpixelrow. -typedef int (*apt_getsamples_t)(void *context, float *samples, int count); - -typedef struct { - float *prow[APT_MAX_HEIGHT]; // Row buffers - int nrow; // Number of rows - int zenith; // Row in image where satellite reaches peak elevation - apt_channel_t chA, chB; // ID of each channel - char name[256]; // Stripped filename - char *palette; // Filename of palette -} apt_image_t; - -typedef struct { - float r, g, b; -} apt_rgb_t; - -int APT_API apt_init(double sample_rate); -int APT_API apt_getpixelrow(float *pixelv, int nrow, int *zenith, int reset, apt_getsamples_t getsamples, void *context); - -void APT_API apt_histogramEqualise(float **prow, int nrow, int offset, int width); -void APT_API apt_linearEnhance(float **prow, int nrow, int offset, int width); -apt_channel_t APT_API apt_calibrate(float **prow, int nrow, int offset, int width); -void APT_API apt_denoise(float **prow, int nrow, int offset, int width); -void APT_API apt_flipImage(apt_image_t *img, int width, int offset); -int APT_API apt_cropNoise(apt_image_t *img); -void APT_API apt_calibrate_thermal(int satnum, apt_image_t *img, int offset, int width); -void APT_API apt_calibrate_visible(int satnum, apt_image_t *img, int offset, int width); -// Moved to apt_calibrate_thermal -#define apt_temperature apt_calibrate_thermal - -apt_rgb_t APT_API apt_applyPalette(char *palette, int val); -apt_rgb_t APT_API apt_RGBcomposite(apt_rgb_t top, float top_a, apt_rgb_t bottom, float bottom_a); - -extern char APT_API apt_TempPalette[256 * 3]; -extern char APT_API apt_PrecipPalette[58 * 3]; - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/argparse b/src/argparse deleted file mode 160000 index c612dc0..0000000 --- a/src/argparse +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c612dc03958cdbd538ca306d61853b643a435933 diff --git a/src/calibration.c b/src/calibration.c deleted file mode 100644 index e5b32fa..0000000 --- a/src/calibration.c +++ /dev/null @@ -1,101 +0,0 @@ -/* - * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "calibration.h" - -#include "util.h" - -const calibration_t calibration[3] = {{.name = "NOAA-15", - .prt = - { - {1.36328e-06f, 0.051045f, 276.60157f}, // PRT 1 - {1.47266e-06f, 0.050909f, 276.62531f}, // PRT 2 - {1.47656e-06f, 0.050907f, 276.67413f}, // PRT 3 - {1.47656e-06f, 0.050966f, 276.59258f} // PRT 4 - }, - .visible = {{.low = {0.0568f, -2.1874f}, .high = {0.1633f, -54.9928f}, .cutoff = 496.0f}, - {.low = {0.0596f, -2.4096f}, .high = {0.1629f, -55.2436f}, .cutoff = 511.0f}}, - .rad = - { - {925.4075f, 0.337810f, 0.998719f}, // Channel 4 - {839.8979f, 0.304558f, 0.999024f}, // Channel 5 - {2695.9743f, 1.621256f, 0.998015f} // Channel 3B - }, - .cor = - { - {-4.50f, {0.0004524f, -0.0932f, 4.76f}}, // Channel 4 - {-3.61f, {0.0002811f, -0.0659f, 3.83f}}, // Channel 5 - {0.0f, {0.0f, 0.0f, 0.0f}} // Channel 3B - }}, - {.name = "NOAA-18", - .prt = - { - {1.657e-06f, 0.05090f, 276.601f}, // PRT 1 - {1.482e-06f, 0.05101f, 276.683f}, // PRT 2 - {1.313e-06f, 0.05117f, 276.565f}, // PRT 3 - {1.484e-06f, 0.05103f, 276.615f} // PRT 4 - }, - .visible = {{.low = {0.06174f, -2.434f}, .high = {0.1841f, -63.31f}, .cutoff = 501.54f}, - {.low = {0.07514f, -2.960f}, .high = {0.2254f, -78.55f}, .cutoff = 500.40f}}, - .rad = - { - {928.1460f, 0.436645f, 0.998607f}, // Channel 4 - {833.2532f, 0.253179f, 0.999057f}, // Channel 5 - {2659.7952f, 1.698704f, 0.996960f} // Channel 3B - }, - .cor = - { - {-5.53f, {0.00052337f, -0.11069f, 5.82f}}, // Channel 4 - {-2.22f, {0.00017715f, -0.04360f, 2.67f}}, // Channel 5 - {0.0f, {0.0f, 0.0f, 0.0f}} // Channel 3B - }}, - {.name = "NOAA-19", - .prt = - { - {1.405783e-06f, 0.051111f, 276.6067f}, // PRT 1 - {1.496037e-06f, 0.051090f, 276.6119f}, // PRT 2 - {1.496990e-06f, 0.051033f, 276.6311f}, // PRT 3 - {1.493110e-06f, 0.051058f, 276.6268f} // PRT 4 - }, - .visible = {{.low = {0.05555f, -2.159f}, .high = {0.1639f, -56.33f}, .cutoff = 496.43f}, - {.low = {0.06614f, -2.565f}, .high = {0.1970f, -68.01f}, .cutoff = 500.37f}}, - .rad = - { - {928.9f, 0.53959f, 0.998534f}, // Channel 4 - {831.9f, 0.36064f, 0.998913f}, // Channel 5 - {2670.0f, 1.67396f, 0.997364f} // Channel 3B - }, - .cor = { - {-5.49f, {0.00054668f, -0.11187f, 5.70f}}, // Channel 4 - {-3.39f, {0.00024985f, -0.05991f, 3.58f}}, // Channel 5 - {0.0f, {0.0f, 0.0f, 0.0f}} // Channel 3B - }}}; - -calibration_t get_calibration(int satid) { - switch (satid) { - case 15: - return calibration[0]; - case 18: - return calibration[1]; - case 19: - return calibration[2]; - default: - error("Invalid satid in get_calibration()"); /* following is only to shut up the compiler */ - return calibration[0]; - } -} diff --git a/src/color.c b/src/color.c deleted file mode 100644 index 50401c4..0000000 --- a/src/color.c +++ /dev/null @@ -1,83 +0,0 @@ -/* - * This file is part of Aptdec. - * Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 - * - * Aptdec is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "color.h" - -#include "apt.h" - -apt_rgb_t apt_applyPalette(char *palette, int val) { - return (apt_rgb_t){palette[(int)CLIP(val, 0, 255) * 3 + 0], palette[(int)CLIP(val, 0, 255) * 3 + 1], - palette[(int)CLIP(val, 0, 255) * 3 + 2]}; -} - -apt_rgb_t apt_RGBcomposite(apt_rgb_t top, float top_a, apt_rgb_t bottom, float bottom_a) { - return (apt_rgb_t){MCOMPOSITE(top.r, top_a, bottom.r, bottom_a), MCOMPOSITE(top.g, top_a, bottom.g, bottom_a), - MCOMPOSITE(top.b, top_a, bottom.b, bottom_a)}; -} - -// The "I totally didn't just steal this from WXtoImg" palette -char apt_TempPalette[256 * 3] = { - "\x45\x0\x8f\x46\x0\x91\x47\x0\x92\x48\x0\x94\x49\x0\x96\x4a\x0\x98\x4b\x0\x9b\x4d\x0\x9d" - "\x4e\x0\xa0\x50\x0\xa2\x51\x0\xa5\x52\x0\xa7\x54\x0\xaa\x56\x0\xae\x57\x0\xb1" - "\x58\x0\xb4\x5a\x0\xb7\x5c\x0\xba\x5e\x0\xbd\x5f\x0\xc0\x61\x0\xc4\x64\x0\xc8" - "\x66\x0\xcb\x68\x0\xce\x69\x0\xd1\x68\x0\xd4\x65\x0\xd7\x63\x0\xda\x61\x0\xdd" - "\x5d\x0\xe1\x5b\x0\xe4\x59\x0\xe6\x56\x0\xe9\x53\x0\xeb\x50\x0\xee\x4d\x0\xf0" - "\x49\x0\xf3\x47\x0\xfc\x43\x0\xfa\x31\x0\xbf\x20\x0\x89\x20\x0\x92\x1e\x0\x95" - "\x1b\x0\x97\x19\x0\x9a\x17\x0\x9c\x15\x0\x9e\x12\x0\xa0\xf\x0\xa3\xf\x2\xa5" - "\xe\x6\xa8\xe\xa\xab\xe\xd\xad\xe\x11\xb1\xd\x15\xb4\xd\x18\xb7\xd\x1c\xba" - "\xb\x21\xbd\xa\x25\xc0\xa\x29\xc3\x9\x2d\xc6\x8\x33\xca\x7\x36\xcd\x7\x3b\xd0" - "\x7\x41\xd3\x5\x45\xd6\x4\x4b\xd9\x4\x50\xdc\x3\x55\xde\x2\x5d\xe2\x1\x61\xe5" - "\x0\x66\xe7\x0\x6c\xea\x0\x72\xec\x0\x78\xee\x0\x7d\xf0\x0\x82\xf3\x0\x8d\xfc" - "\x0\x90\xfa\x0\x71\xbf\x0\x54\x89\x0\x5c\x91\x0\x61\x94\x0\x64\x96\x0\x68\x97" - "\x0\x6d\x99\x0\x71\x9b\x0\x75\x9d\x0\x79\x9f\x0\x7e\xa0\x0\x82\xa2\x0\x87\xa4" - "\x0\x8c\xa6\x0\x92\xaa\x0\x96\xac\x0\x9c\xae\x0\xa2\xb1\x0\xa6\xb3\x0\xaa\xb5" - "\x0\xad\xb7\x0\xb1\xba\x0\xb6\xbe\x0\xba\xc0\x0\xbe\xc2\x0\xc2\xc5\x0\xc6\xc6" - "\x0\xca\xc9\x0\xcc\xca\x0\xcf\xcb\x0\xd2\xcc\x0\xd4\xcc\x0\xd6\xcc\x0\xd9\xcb" - "\x0\xdb\xcb\x0\xde\xcb\x0\xe0\xcb\x0\xe2\xcc\x0\xea\xd2\x0\xea\xcf\x0\xb9\xa4" - "\x0\x8e\x7a\x1\x94\x7c\x4\x97\x79\x7\x99\x75\x9\x9b\x71\xd\x9d\x6b\x10\x9f\x67" - "\x12\xa1\x63\x15\xa3\x5f\x17\xa5\x59\x1a\xa8\x55\x1d\xaa\x50\x20\xac\x4b\x24\xaf\x45" - "\x28\xb2\x41\x2b\xb5\x3b\x2e\xb8\x35\x31\xba\x30\x34\xbd\x2b\x39\xbf\x24\x3f\xc1\x17" - "\x49\xc5\x8\x4f\xc8\x1\x4f\xca\x0\x4e\xcd\x0\x4e\xcf\x0\x4f\xd2\x0\x54\xd5\x0" - "\x5d\xd8\x0\x68\xdb\x0\x6e\xdd\x0\x74\xdf\x0\x7a\xe2\x0\x7f\xe4\x0\x85\xe7\x0" - "\x8b\xe9\x0\x8f\xeb\x0\x9b\xf3\x0\x9e\xf2\x0\x7e\xbb\x0\x60\x8a\x0\x68\x92\x0" - "\x6d\x95\x0\x71\x96\x0\x75\x98\x0\x7b\x9a\x0\x7f\x9d\x0\x83\x9f\x0\x87\xa1\x0" - "\x8c\xa2\x0\x8f\xa5\x0\x92\xa7\x0\x96\xa9\x0\x9a\xad\x0\x9d\xb0\x0\xa1\xb2\x0" - "\xa5\xb5\x0\xa9\xb7\x0\xad\xba\x0\xb2\xbd\x0\xb6\xbf\x0\xbb\xc3\x0\xbf\xc6\x0" - "\xc3\xc8\x0\xc8\xcb\x0\xcc\xce\x0\xd0\xd1\x0\xd3\xd2\x0\xd5\xd4\x0\xd9\xd4\x0" - "\xdc\xd4\x0\xde\xd5\x0\xe1\xd5\x0\xe3\xd5\x0\xe6\xd4\x0\xe8\xd1\x0\xea\xce\x0" - "\xf2\xcf\x0\xf2\xca\x0\xbb\x99\x0\x8a\x6e\x0\x92\x72\x0\x95\x72\x0\x97\x71\x0" - "\x9a\x70\x0\x9c\x6e\x0\x9e\x6d\x0\xa0\x6b\x0\xa3\x6a\x0\xa5\x68\x0\xa8\x67\x0" - "\xab\x66\x0\xae\x65\x0\xb2\x63\x0\xb4\x61\x0\xb7\x5f\x0\xba\x5d\x0\xbd\x5c\x0" - "\xc0\x59\x0\xc3\x57\x0\xc6\x54\x0\xca\x50\x0\xcd\x4d\x0\xd0\x4a\x0\xd3\x47\x0" - "\xd6\x43\x0\xd9\x40\x0\xdc\x3d\x0\xde\x39\x0\xe2\x33\x0\xe5\x2f\x0\xe7\x2c\x0" - "\xea\x28\x0\xec\x23\x0\xef\x1f\x0\xf1\x1a\x0\xf3\x14\x0\xfb\xf\x0\xfa\xd\x0" - "\xc1\x5\x0\x8e\x0\x0\x97\x0\x0\x9b\x0\x0\x9e\x0\x0\xa1\x0\x0\xa5\x0\x0" - "\xa9\x0\x0\xad\x0\x0\xb1\x0\x0\xb6\x0\x0\xba\x0\x0\xbd\x0\x0\xc2\x0\x0" - "\xc8\x0\x0\xcc\x0\x0\xcc\x0\x0"}; - -char apt_PrecipPalette[58 * 3] = { - "\x8\x89\x41\x0\xc5\x44\x0\xd1\x2c\x0\xe3\x1c\x0\xf9\x6\x14\xff\x0\x3e\xff\x0\x5d\xff\x0" - "\x80\xff\x0\xab\xff\x0\xcd\xfe\x0\xf8\xff\x0\xff\xe6\x0\xff\xb8\x0\xff\x98\x0" - "\xff\x75\x0\xff\x49\x0\xfe\x26\x0\xff\x4\x0\xdf\x0\x0\xa8\x0\x0\x87\x0\x0" - "\x5a\x0\x0\x39\x0\x0\x11\x0\x0\xe\x10\x10\x23\x22\x22\x33\x33\x33\x41\x41\x41" - "\x53\x53\x53\x60\x60\x60\x6e\x6e\x6e\x80\x80\x80\x8e\x8e\x8e\xa0\xa0\xa0\xae\xae\xae" - "\xc0\xc0\xc0\xce\xce\xce\xdc\xdc\xdc\xef\xef\xef\xfa\xfa\xfa\xff\xff\xff\xff\xff\xff" - "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" - "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" - "\xff\xff\xff"}; diff --git a/src/color.h b/src/color.h deleted file mode 100644 index 1fc253e..0000000 --- a/src/color.h +++ /dev/null @@ -1,3 +0,0 @@ -#include "common.h" - -#define MCOMPOSITE(m1, a1, m2, a2) (m1 * a1 + m2 * a2 * (1 - a1)) diff --git a/src/common.h b/src/common.h deleted file mode 100644 index 7130d46..0000000 --- a/src/common.h +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of Aptdec. - * Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 - * - * Aptdec is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -// Constants -#define VERSION "Aptdec; (c) 2004-2009 Thierry Leconte F4DWV, Xerbo (xerbo@protonmail.com) 2019-2022" - -// Useful macros -#define CLIP(v, lo, hi) (v > hi ? hi : (v > lo ? v : lo)) -#define CONTAINS(str, char) (strchr(str, (int)char) != NULL) - -// Typedefs -#ifndef STRUCTS_DEFINED -#define STRUCTS_DEFINED -typedef struct { - char *type; // Output image type - char *effects; // Effects on the image - int satnum; // The satellite number - char *path; // Output directory - int realtime; // Realtime decoding - char *filename; // Output filename - char *palette; // Filename of palette - float gamma; // Gamma -} options_t; - -enum imagetypes { - Raw_Image = 'r', - Palleted = 'p', - Temperature = 't', - Channel_A = 'a', - Channel_B = 'b', - Distribution = 'd', - Visible = 'v' -}; -enum effects { - Crop_Telemetry = 't', - Histogram_Equalise = 'h', - Denoise = 'd', - Precipitation_Overlay = 'p', - Flip_Image = 'f', - Linear_Equalise = 'l', - Crop_Noise = 'c' -}; - -#endif diff --git a/src/dsp.c b/src/dsp.c deleted file mode 100644 index befffe7..0000000 --- a/src/dsp.c +++ /dev/null @@ -1,258 +0,0 @@ -/* - * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2022 Xerbo (xerbo@protonmail.com) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include - -#include "apt.h" -#include "filter.h" -#include "taps.h" -#include "util.h" - -// Block sizes -#define BLKAMP 32768 -#define BLKIN 32768 - -#define CARRIER_FREQ 2400.0 -#define MAX_CARRIER_OFFSET 20.0 - -#define RSMULT 15 -#define Fi (APT_IMG_WIDTH * 2 * RSMULT) - -static float _sample_rate; - -static float offset = 0.0; -static float FreqLine = 1.0; - -static float oscillator_freq; -static float pll_alpha; -static float pll_beta; - -// Initalise and configure PLL -int apt_init(double sample_rate) { - if (sample_rate > Fi) return 1; - if (sample_rate < APT_IMG_WIDTH * 2) return -1; - _sample_rate = sample_rate; - - // Pll configuration - pll_alpha = 50 / _sample_rate; - pll_beta = pll_alpha * pll_alpha / 2.0; - oscillator_freq = CARRIER_FREQ / sample_rate; - - return 0; -} - -static float pll(complexf_t in) { - static float oscillator_phase = 0.0; - - // Internal oscillator -#ifdef _MSC_VER - complexf_t osc = _FCbuild(cos(oscillator_phase), -sin(oscillator_phase)); - in = _FCmulcc(in, osc); -#else - complexf_t osc = cos(oscillator_phase) + -sin(oscillator_phase) * I; - in *= osc; -#endif - - // Error detector - float error = cargf(in); - - // Adjust frequency and phase - oscillator_freq += pll_beta * error; - oscillator_freq = clamp_half(oscillator_freq, (CARRIER_FREQ + MAX_CARRIER_OFFSET) / _sample_rate); - oscillator_phase += M_TAUf * (pll_alpha * error + oscillator_freq); - oscillator_phase = remainderf(oscillator_phase, M_TAUf); - - return crealf(in); -} - -// Convert samples into pixels -static int getamp(float *ampbuff, int count, apt_getsamples_t getsamples, void *context) { - static float inbuff[BLKIN]; - static int idxin = 0; - static int nin = 0; - - for (int n = 0; n < count; n++) { - float I2, Q; - - // Get some more samples when needed - if (nin < HILBERT_FILTER_SIZE * 2 + 2) { - // Number of samples read - int res; - memmove(inbuff, &(inbuff[idxin]), nin * sizeof(float)); - idxin = 0; - - // Read some samples - res = getsamples(context, &(inbuff[nin]), BLKIN - nin); - nin += res; - - // Make sure there is enough samples to continue - if (nin < HILBERT_FILTER_SIZE * 2 + 2) return n; - } - - // Process read samples into a brightness value - complexf_t sample = hilbert_transform(&inbuff[idxin], hilbert_filter, HILBERT_FILTER_SIZE); - ampbuff[n] = pll(sample); - - // Increment current sample - idxin++; - nin--; - } - - return count; -} - -// Sub-pixel offsetting -int getpixelv(float *pvbuff, int count, apt_getsamples_t getsamples, void *context) { - // Amplitude buffer - static float ampbuff[BLKAMP]; - static int nam = 0; - static int idxam = 0; - - float mult; - - // Gaussian resampling factor - mult = (float)Fi / _sample_rate * FreqLine; - int m = (int)(LOW_PASS_SIZE / mult + 1); - - for (int n = 0; n < count; n++) { - int shift; - - if (nam < m) { - int res; - memmove(ampbuff, &(ampbuff[idxam]), nam * sizeof(float)); - idxam = 0; - res = getamp(&(ampbuff[nam]), BLKAMP - nam, getsamples, context); - nam += res; - if (nam < m) return n; - } - - pvbuff[n] = interpolating_convolve(&(ampbuff[idxam]), low_pass, LOW_PASS_SIZE, offset, mult) * mult * 256.0; - - shift = ((int)floor((RSMULT - offset) / mult)) + 1; - offset = shift * mult + offset - RSMULT; - - idxam += shift; - nam -= shift; - } - - return count; -} - -// Get an entire row of pixels, aligned with sync markers -int apt_getpixelrow(float *pixelv, int nrow, int *zenith, int reset, apt_getsamples_t getsamples, void *context) { - static float pixels[APT_IMG_WIDTH + SYNC_PATTERN_SIZE]; - static size_t npv; - static int synced = 0; - static float max = 0.0; - static float minDoppler = 1000000000, previous = 0; - - if (reset) synced = 0; - - float corr, ecorr, lcorr; - int res; - - // Move the row buffer into the the image buffer - if (npv > 0) memmove(pixelv, pixels, npv * sizeof(float)); - - // Get the sync line - if (npv < SYNC_PATTERN_SIZE + 2) { - res = getpixelv(&(pixelv[npv]), SYNC_PATTERN_SIZE + 2 - npv, getsamples, context); - npv += res; - if (npv < SYNC_PATTERN_SIZE + 2) return 0; - } - - // Calculate the frequency offset - ecorr = convolve(pixelv, sync_pattern, SYNC_PATTERN_SIZE); - corr = convolve(&pixelv[1], sync_pattern, SYNC_PATTERN_SIZE - 1); - lcorr = convolve(&pixelv[2], sync_pattern, SYNC_PATTERN_SIZE - 2); - FreqLine = 1.0 + ((ecorr - lcorr) / corr / APT_IMG_WIDTH / 4.0); - - float val = fabs(lcorr - ecorr) * 0.25 + previous * 0.75; - if (val < minDoppler && nrow > 10) { - minDoppler = val; - *zenith = nrow; - } - previous = fabs(lcorr - ecorr); - - // The point in which the pixel offset is recalculated - if (corr < 0.75 * max) { - synced = 0; - FreqLine = 1.0; - } - max = corr; - - if (synced < 8) { - int mshift; - static int lastmshift; - - if (npv < APT_IMG_WIDTH + SYNC_PATTERN_SIZE) { - res = getpixelv(&(pixelv[npv]), APT_IMG_WIDTH + SYNC_PATTERN_SIZE - npv, getsamples, context); - npv += res; - if (npv < APT_IMG_WIDTH + SYNC_PATTERN_SIZE) return 0; - } - - // Test every possible position until we get the best result - mshift = 0; - for (int shift = 0; shift < APT_IMG_WIDTH; shift++) { - float corr; - - corr = convolve(&(pixelv[shift + 1]), sync_pattern, SYNC_PATTERN_SIZE); - if (corr > max) { - mshift = shift; - max = corr; - } - } - - // Stop rows dissapearing into the void - int mshiftOrig = mshift; - if (abs(lastmshift - mshift) > 3 && nrow != 0) { - mshift = 0; - } - lastmshift = mshiftOrig; - - // If we are already as aligned as we can get, just continue - if (mshift == 0) { - synced++; - } else { - memmove(pixelv, &(pixelv[mshift]), (npv - mshift) * sizeof(float)); - npv -= mshift; - synced = 0; - FreqLine = 1.0; - } - } - - // Get the rest of this row - if (npv < APT_IMG_WIDTH) { - res = getpixelv(&(pixelv[npv]), APT_IMG_WIDTH - npv, getsamples, context); - npv += res; - if (npv < APT_IMG_WIDTH) return 0; - } - - // Move the sync lines into the output buffer with the calculated offset - if (npv == APT_IMG_WIDTH) { - npv = 0; - } else { - memmove(pixels, &(pixelv[APT_IMG_WIDTH]), (npv - APT_IMG_WIDTH) * sizeof(float)); - npv -= APT_IMG_WIDTH; - } - - return 1; -} diff --git a/src/filter.c b/src/filter.c deleted file mode 100644 index 06746aa..0000000 --- a/src/filter.c +++ /dev/null @@ -1,62 +0,0 @@ -/* - * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2022 Xerbo (xerbo@protonmail.com) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "filter.h" - -#include - -#include "util.h" - -float convolve(const float *in, const float *taps, size_t len) { - float sum = 0.0; - for (size_t i = 0; i < len; i++) { - sum += in[i] * taps[i]; - } - - return sum; -} - -complexf_t hilbert_transform(const float *in, const float *taps, size_t len) { - float i = 0.0; - float q = 0.0; - - for (size_t k = 0; k < len; k++) { - q += in[2 * k] * taps[k]; - i += in[2 * k]; - } - - i = in[len - 1] - (i / len); -#ifdef _MSC_VER - return _FCbuild(i, q); -#else - return i + q * I; -#endif -} - -float interpolating_convolve(const float *in, const float *taps, size_t len, float offset, float delta) { - float out = 0.0; - float n = offset; - - for (size_t i = 0; i < (len - 1) / delta - 1; n += delta, i++) { - int k = (int)floor(n); - float alpha = n - k; - - out += in[i] * (taps[k] * (1.0f - alpha) + taps[k + 1] * alpha); - } - return out; -} diff --git a/src/image.c b/src/image.c deleted file mode 100644 index 8c9cbc9..0000000 --- a/src/image.c +++ /dev/null @@ -1,388 +0,0 @@ -/* - * This file is part of Aptdec. - * Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 - * - * Aptdec is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "image.h" - -#include -#include -#include -#include - -#include "algebra.h" -#include "apt.h" -#include "util.h" - -static linear_t compute_regression(float *wedges) { - // { 0.106, 0.215, 0.324, 0.433, 0.542, 0.652, 0.78, 0.87, 0.0 } - const float teleramp[9] = {31.07, 63.02, 94.96, 126.9, 158.86, 191.1, 228.62, 255.0, 0.0}; - - return linear_regression(wedges, teleramp, 9); -} - -static float tele[16]; -static float Cs; - -void apt_histogramEqualise(float **prow, int nrow, int offset, int width) { - // Plot histogram - int histogram[256] = {0}; - for (int y = 0; y < nrow; y++) - for (int x = 0; x < width; x++) histogram[(int)CLIP(prow[y][x + offset], 0, 255)]++; - - // Calculate cumulative frequency - long sum = 0, cf[256] = {0}; - for (int i = 0; i < 255; i++) { - sum += histogram[i]; - cf[i] = sum; - } - - // Apply histogram - int area = nrow * width; - for (int y = 0; y < nrow; y++) { - for (int x = 0; x < width; x++) { - int k = (int)prow[y][x + offset]; - prow[y][x + offset] = (256.0f / area) * cf[k]; - } - } -} - -void apt_linearEnhance(float **prow, int nrow, int offset, int width) { - // Plot histogram - int histogram[256] = {0}; - for (int y = 0; y < nrow; y++) - for (int x = 0; x < width; x++) histogram[(int)CLIP(prow[y][x + offset], 0, 255)]++; - - // Find min/max points - int min = -1, max = -1; - for (int i = 5; i < 250; i++) { - if (histogram[i] / width / (nrow / 255.0) > 0.1) { - if (min == -1) min = i; - max = i; - } - } - - // Stretch the brightness into the new range - for (int y = 0; y < nrow; y++) { - for (int x = 0; x < width; x++) { - prow[y][x + offset] = (prow[y][x + offset] - min) / (max - min) * 255.0f; - prow[y][x + offset] = CLIP(prow[y][x + offset], 0.0f, 255.0f); - } - } -} - -// Brightness calibrate, including telemetry -void calibrateImage(float **prow, int nrow, int offset, int width, linear_t regr) { - offset -= APT_SYNC_WIDTH + APT_SPC_WIDTH; - - for (int y = 0; y < nrow; y++) { - for (int x = 0; x < width + APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_TELE_WIDTH; x++) { - float pv = linear_calc(prow[y][x + offset], regr); - prow[y][x + offset] = CLIP(pv, 0, 255); - } - } -} - -float teleNoise(float *wedges) { - float pattern[9] = {31.07, 63.02, 94.96, 126.9, 158.86, 191.1, 228.62, 255.0, 0.0}; - float noise = 0; - for (int i = 0; i < 9; i++) noise += fabs(wedges[i] - pattern[i]); - - return noise; -} - -// Get telemetry data for thermal calibration -apt_channel_t apt_calibrate(float **prow, int nrow, int offset, int width) { - float teleline[APT_MAX_HEIGHT] = {0.0}; - float wedge[16]; - linear_t regr[APT_MAX_HEIGHT / APT_FRAME_LEN + 1]; - int telestart, mtelestart = 0; - int channel = -1; - - // The minimum rows required to decode a full frame - if (nrow < APT_CALIBRATION_ROWS) { - error_noexit("Telemetry decoding error, not enough rows"); - return APT_CHANNEL_UNKNOWN; - } - - // Calculate average of a row of telemetry - for (int y = 0; y < nrow; y++) { - for (int x = 3; x < 43; x++) teleline[y] += prow[y][x + offset + width]; - - teleline[y] /= 40.0; - } - - /* Wedge 7 is white and 8 is black, this will have the largest - * difference in brightness, this can be used to find the current - * position within the telemetry. - */ - float max = 0.0f; - for (int n = nrow / 3 - 64; n < 2 * nrow / 3 - 64; n++) { - float df; - - // (sum 4px below) - (sum 4px above) - df = (float)((teleline[n - 4] + teleline[n - 3] + teleline[n - 2] + teleline[n - 1]) - - (teleline[n + 0] + teleline[n + 1] + teleline[n + 2] + teleline[n + 3])); - - // Find the maximum difference - if (df > max) { - mtelestart = n; - max = df; - } - } - - telestart = (mtelestart + 64) % APT_FRAME_LEN; - - // Make sure that theres at least one full frame in the image - if (nrow < telestart + APT_FRAME_LEN) { - error_noexit("Telemetry decoding error, not enough rows"); - return APT_CHANNEL_UNKNOWN; - } - - // Find the least noisy frame - float minNoise = -1; - int bestFrame = -1; - for (int n = telestart, k = 0; n < nrow - APT_FRAME_LEN; n += APT_FRAME_LEN, k++) { - int j; - - for (j = 0; j < 16; j++) { - int i; - - wedge[j] = 0.0; - for (i = 1; i < 7; i++) wedge[j] += teleline[n + j * 8 + i]; - wedge[j] /= 6; - } - - float noise = teleNoise(wedge); - if (noise < minNoise || minNoise == -1) { - minNoise = noise; - bestFrame = k; - - // Compute & apply regression on the wedges - regr[k] = compute_regression(wedge); - for (int j = 0; j < 16; j++) tele[j] = linear_calc(wedge[j], regr[k]); - - /* Compare the channel ID wedge to the reference - * wedges, the wedge with the closest match will - * be the channel ID - */ - float min = -1; - for (int j = 0; j < 6; j++) { - float df = (float)(tele[15] - tele[j]); - df *= df; - - if (df < min || min == -1) { - channel = j; - min = df; - } - } - - // Find the brightness of the minute marker, I don't really know what for - Cs = 0.0; - int i, j = n; - for (i = 0, j = n; j < n + APT_FRAME_LEN; j++) { - float csline = 0.0; - for (int l = 3; l < 43; l++) csline += prow[n][l + offset - APT_SPC_WIDTH]; - csline /= 40.0; - - if (csline > 50.0) { - Cs += csline; - i++; - } - } - Cs = linear_calc((Cs / i), regr[k]); - } - } - - if (bestFrame == -1) { - error_noexit("Something has gone very wrong, please file a bug report"); - return APT_CHANNEL_UNKNOWN; - } - - calibrateImage(prow, nrow, offset, width, regr[bestFrame]); - - return (apt_channel_t)(channel + 1); -} - -extern float quick_select(float arr[], int n); - -// Biased median denoise, pretyt ugly -#define TRIG_LEVEL 40 -void apt_denoise(float **prow, int nrow, int offset, int width) { - for (int y = 2; y < nrow - 2; y++) { - for (int x = offset + 1; x < offset + width - 1; x++) { - if (prow[y][x + 1] - prow[y][x] > TRIG_LEVEL || prow[y][x - 1] - prow[y][x] > TRIG_LEVEL || - prow[y + 1][x] - prow[y][x] > TRIG_LEVEL || prow[y - 1][x] - prow[y][x] > TRIG_LEVEL) { - prow[y][x] = quick_select((float[]){prow[y + 2][x - 1], prow[y + 2][x], prow[y + 2][x + 1], prow[y + 1][x - 1], - prow[y + 1][x], prow[y + 1][x + 1], prow[y - 1][x - 1], prow[y - 1][x], - prow[y - 1][x + 1], prow[y - 2][x - 1], prow[y - 2][x], prow[y - 2][x + 1]}, - 12); - } - } - } -} -#undef TRIG_LEVEL - -// Flips a channel, for northbound passes -void apt_flipImage(apt_image_t *img, int width, int offset) { - for (int y = 1; y < img->nrow; y++) { - for (int x = 1; x < ceil(width / 2.0); x++) { - // Flip top-left & bottom-right - float buffer = img->prow[img->nrow - y][offset + x]; - img->prow[img->nrow - y][offset + x] = img->prow[y][offset + (width - x)]; - img->prow[y][offset + (width - x)] = buffer; - } - } -} - -// Calculate crop to reomve noise from the start and end of an image -int apt_cropNoise(apt_image_t *img) { -#define NOISE_THRESH 180.0 - - // Average value of minute marker - float spc_rows[APT_MAX_HEIGHT] = {0.0}; - int startCrop = 0; - int endCrop = img->nrow; - for (int y = 0; y < img->nrow; y++) { - for (int x = 0; x < APT_SPC_WIDTH; x++) { - spc_rows[y] += img->prow[y][x + (APT_CHB_OFFSET - APT_SPC_WIDTH)]; - } - spc_rows[y] /= APT_SPC_WIDTH; - - // Skip minute markings - if (spc_rows[y] < 10) { - spc_rows[y] = spc_rows[y - 1]; - } - } - - // 3 row average - for (int y = 0; y < img->nrow; y++) { - spc_rows[y] = (spc_rows[y + 1] + spc_rows[y + 2] + spc_rows[y + 3]) / 3; - // img.prow[y][0] = spc_rows[y]; - } - - // Find ends - for (int y = 0; y < img->nrow - 1; y++) { - if (spc_rows[y] > NOISE_THRESH) { - endCrop = y; - } - } - for (int y = img->nrow; y > 0; y--) { - if (spc_rows[y] > NOISE_THRESH) { - startCrop = y; - } - } - - // printf("Crop rows: %i -> %i\n", startCrop, endCrop); - - // Remove the noisy rows at start - for (int y = 0; y < img->nrow - startCrop; y++) { - memmove(img->prow[y], img->prow[y + startCrop], sizeof(float) * APT_PROW_WIDTH); - } - - // Ignore the noisy rows at the end - img->nrow = (endCrop - startCrop); - - return startCrop; -} - -// --- Visible and Temperature Calibration --- // -#include "calibration.h" - -typedef struct { - float Nbb; - float Cs; - float Cb; - int ch; -} tempparam_t; - -// IR channel temperature compensation -tempparam_t tempcomp(float t[16], int ch, int satnum) { - tempparam_t tpr; - tpr.ch = ch - 4; - - const calibration_t calibration = get_calibration(satnum); - const float Vc = calibration.rad[tpr.ch].vc; - const float A = calibration.rad[tpr.ch].A; - const float B = calibration.rad[tpr.ch].B; - - // Compute PRT temperature - float T[4]; - for (size_t n = 0; n < 4; n++) { - T[n] = quadratic_calc(t[n + 9] * 4.0, calibration.prt[n]); - } - - float Tbb = (T[0] + T[1] + T[2] + T[3]) / 4.0; // Blackbody temperature - float Tbbstar = A + Tbb * B; // Effective blackbody temperature - - tpr.Nbb = C1 * pow(Vc, 3) / (expf(C2 * Vc / Tbbstar) - 1.0f); // Blackbody radiance - - tpr.Cs = 246.4 * 4.0; // FIXME - tpr.Cb = t[14] * 4.0; - return tpr; -} - -// IR channel temperature calibration -static float tempcal(float Ce, int satnum, tempparam_t tpr) { - const calibration_t calibration = get_calibration(satnum); - const float Ns = calibration.cor[tpr.ch].Ns; - const float Vc = calibration.rad[tpr.ch].vc; - const float A = calibration.rad[tpr.ch].A; - const float B = calibration.rad[tpr.ch].B; - - float Nl = Ns + (tpr.Nbb - Ns) * (tpr.Cs - Ce * 4.0) / (tpr.Cs - tpr.Cb); // Linear radiance estimate - float Nc = quadratic_calc(Nl, calibration.cor[tpr.ch].quadratic); // Non-linear correction - float Ne = Nl + Nc; // Corrected radiance - - float Testar = C2 * Vc / logf(C1 * powf(Vc, 3) / Ne + 1.0); // Equivlent black body temperature - float Te = (Testar - A) / B; // Temperature (kelvin) - - // Convert to celsius - Te -= 273.15; - // Rescale to 0-255 for -100°C to +60°C - return (Te + 100.0) / 160.0 * 255.0; -} - -// Temperature calibration wrapper -void apt_calibrate_thermal(int satnum, apt_image_t *img, int offset, int width) { - tempparam_t temp = tempcomp(tele, img->chB, satnum); - - for (int y = 0; y < img->nrow; y++) { - for (int x = 0; x < width; x++) { - img->prow[y][x + offset] = (float)tempcal(img->prow[y][x + offset], satnum, temp); - } - } -} - -float calibrate_pixel(float value, int channel, calibration_t cal) { - if (value > cal.visible[channel].cutoff) { - return linear_calc(value * 4.0f, cal.visible[channel].high) * 255.0f / 100.0f; - } else { - return linear_calc(value * 4.0f, cal.visible[channel].low) * 255.0f / 100.0f; - } -} - -void apt_calibrate_visible(int satnum, apt_image_t *img, int offset, int width) { - const calibration_t calibration = get_calibration(satnum); - int channel = img->chA - 1; - - for (int y = 0; y < img->nrow; y++) { - for (int x = 0; x < width; x++) { - img->prow[y][x + offset] = clamp(calibrate_pixel(img->prow[y][x + offset], channel, calibration), 255.0f, 0.0f); - } - } -} diff --git a/src/image.h b/src/image.h deleted file mode 100644 index 94d52eb..0000000 --- a/src/image.h +++ /dev/null @@ -1,2 +0,0 @@ -#include "apt.h" -#include "common.h" diff --git a/src/libs/median.c b/src/libs/median.c deleted file mode 100644 index 2d4e510..0000000 --- a/src/libs/median.c +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This Quickselect routine is based on the algorithm described in - * "Numerical recipes in C", Second Edition, - * Cambridge University Press, 1992, Section 8.5, ISBN 0-521-43108-5 - * This code by Nicolas Devillard - 1998. Public domain. - */ - -#define ELEM_SWAP(a, b) { register float t = (a); (a) = (b); (b) = t; } -float quick_select(float arr[], int n) { - int low, median, high; - int middle, ll, hh; - - low = 0; high = n-1; median = (low + high) / 2; - for (;;) { - if (high <= low) /* One element only */ - return arr[median] ; - - if (high == low + 1) { /* Two elements only */ - if (arr[low] > arr[high]) - ELEM_SWAP(arr[low], arr[high]); - return arr[median]; - } - - /* Find median of low, middle and high items; swap into position low */ - middle = (low + high) / 2; - if (arr[middle] > arr[high]) ELEM_SWAP(arr[middle], arr[high]); - if (arr[low] > arr[high]) ELEM_SWAP(arr[low], arr[high]); - if (arr[middle] > arr[low]) ELEM_SWAP(arr[middle], arr[low]); - - /* Swap low item (now in position middle) into position (low+1) */ - ELEM_SWAP(arr[middle], arr[low+1]); - - /* Nibble from each end towards middle, swapping items when stuck */ - ll = low + 1; - hh = high; - for (;;) { - do ll++; while (arr[low] > arr[ll]); - do hh--; while (arr[hh] > arr[low]); - - if (hh < ll) - break; - - ELEM_SWAP(arr[ll], arr[hh]); - } - - /* Swap middle item (in position low) back into correct position */ - ELEM_SWAP(arr[low], arr[hh]); - - /* Re-set active partition */ - if (hh <= median) - low = ll; - if (hh >= median) - high = hh - 1; - } -} -#undef ELEM_SWAP diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 1ebc488..0000000 --- a/src/main.c +++ /dev/null @@ -1,316 +0,0 @@ -/* - * This file is part of Aptdec. - * Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 - * - * Aptdec is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include -#include -#include -#ifndef _MSC_VER -#include -#else -#include -#endif -#include -#include -#include -#include - -#include "apt.h" -#include "argparse/argparse.h" -#include "color.h" -#include "common.h" -#include "image.h" -#include "pngio.h" -#include "util.h" - -// Audio file -static SNDFILE *audioFile; -// Number of channels in audio file -int channels = 1; - -// Function declarations -static int initsnd(char *filename); -int getsamples(void *context, float *samples, int nb); -static int processAudio(char *filename, options_t *opts); - -#ifdef _MSC_VER -// Functions not supported by MSVC -static char *dirname(char *path) { - static char dir[MAX_PATH]; - _splitpath(path, NULL, dir, NULL, NULL); - return dir; -} - -static char *basename(char *path) { - static char base[MAX_PATH]; - _splitpath(path, NULL, NULL, base, NULL); - return base; -} -#endif - -int main(int argc, const char **argv) { - options_t opts = { - .type = "r", .effects = "", .satnum = 19, .path = ".", .realtime = 0, .filename = "", .palette = "", .gamma = 1.0}; - - static const char *const usages[] = { - "aptdec [options] [[--] sources]", - "aptdec [sources]", - NULL, - }; - - struct argparse_option options[] = { - OPT_HELP(), - OPT_GROUP("Image options"), - OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0), - OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0), - OPT_FLOAT('g', "gamma", &opts.gamma, "gamma adjustment (1.0 = off)", NULL, 0, 0), - - OPT_GROUP("Satellite options"), - OPT_INTEGER('s', "satellite", &opts.satnum, "satellite ID, must be between 15 and 19", NULL, 0, 0), - - OPT_GROUP("Paths"), - OPT_STRING('p', "palette", &opts.palette, "path to a palette", NULL, 0, 0), - OPT_STRING('o', "filename", &opts.filename, "filename of the output image", NULL, 0, 0), - OPT_STRING('d', "output", &opts.path, "output directory (must exist first)", NULL, 0, 0), - - OPT_GROUP("Misc"), - OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0), - OPT_END(), - }; - - struct argparse argparse; - argparse_init(&argparse, options, usages, 0); - argparse_describe( - &argparse, "\nA lightweight FOSS NOAA APT satellite imagery decoder.", - "\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions."); - argc = argparse_parse(&argparse, argc, argv); - - if (argc == 0) { - argparse_usage(&argparse); - } - - // Actually decode the files - for (int i = 0; i < argc; i++) { - char *filename = strdup(argv[i]); - processAudio(filename, &opts); - } - - return 0; -} - -static int processAudio(char *filename, options_t *opts) { - // Image info struct - apt_image_t img; - - // Mapping between wedge value and channel ID - static struct { - char *id[7]; - char *name[7]; - } ch = {{"?", "1", "2", "3A", "4", "5", "3B"}, - {"unknown", "visble", "near-infrared", "near-infrared", "thermal-infrared", "thermal-infrared", "mid-infrared"}}; - - // Buffer for image channel - char desc[60]; - - // Parse file path - char path[256], extension[32]; - strcpy(path, filename); - strcpy(path, dirname(path)); - sscanf(basename(filename), "%255[^.].%31s", img.name, extension); - - if (opts->realtime) { - // Set output filename to current time when in realtime mode - time_t t; - time(&t); - strncpy(img.name, ctime(&t), 24); - - // Init a row writer - initWriter(opts, &img, APT_IMG_WIDTH, APT_MAX_HEIGHT, "Unprocessed realtime image", "r"); - } - - if (strcmp(extension, "png") == 0) { - // Read PNG into image buffer - printf("Reading %s\n", filename); - if (readRawImage(filename, img.prow, &img.nrow) == 0) { - exit(EPERM); - } - } else { - // Attempt to open the audio file - if (initsnd(filename) == 0) exit(EPERM); - - // Build image - // TODO: multithreading, would require some sort of input buffer - for (img.nrow = 0; img.nrow < APT_MAX_HEIGHT; img.nrow++) { - // Allocate memory for this row - img.prow[img.nrow] = (float *)malloc(sizeof(float) * APT_PROW_WIDTH); - - // Write into memory and break the loop when there are no more samples to read - if (apt_getpixelrow(img.prow[img.nrow], img.nrow, &img.zenith, (img.nrow == 0), getsamples, NULL) == 0) break; - - if (opts->realtime) pushRow(img.prow[img.nrow], APT_IMG_WIDTH); - - fprintf(stderr, "Row: %d\r", img.nrow); - fflush(stderr); - } - - // Close stream - sf_close(audioFile); - } - - if (opts->realtime) closeWriter(); - - printf("Total rows: %d\n", img.nrow); - - // Calibrate - img.chA = apt_calibrate(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); - img.chB = apt_calibrate(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); - printf("Channel A: %s (%s)\n", ch.id[img.chA], ch.name[img.chA]); - printf("Channel B: %s (%s)\n", ch.id[img.chB], ch.name[img.chB]); - - // Crop noise from start and end of image - if (CONTAINS(opts->effects, Crop_Noise)) { - img.zenith -= apt_cropNoise(&img); - } - - // Denoise - if (CONTAINS(opts->effects, Denoise)) { - apt_denoise(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); - apt_denoise(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); - } - - // Flip, for northbound passes - if (CONTAINS(opts->effects, Flip_Image)) { - apt_flipImage(&img, APT_CH_WIDTH, APT_CHA_OFFSET); - apt_flipImage(&img, APT_CH_WIDTH, APT_CHB_OFFSET); - } - - // Temperature - if (CONTAINS(opts->type, Temperature) && img.chB >= 4) { - // Create another buffer as to not modify the orignal - apt_image_t tmpimg = img; - for (int i = 0; i < img.nrow; i++) { - tmpimg.prow[i] = (float *)malloc(sizeof(float) * APT_PROW_WIDTH); - memcpy(tmpimg.prow[i], img.prow[i], sizeof(float) * APT_PROW_WIDTH); - } - - // Perform temperature calibration - apt_calibrate_thermal(opts->satnum, &tmpimg, APT_CHB_OFFSET, APT_CH_WIDTH); - ImageOut(opts, &tmpimg, APT_CHB_OFFSET, APT_CH_WIDTH, "Temperature", Temperature, (char *)apt_TempPalette); - } - - // Visible - if (CONTAINS(opts->type, Visible) && img.chA <= 2) { - // Create another buffer as to not modify the orignal - apt_image_t tmpimg = img; - for (int i = 0; i < img.nrow; i++) { - tmpimg.prow[i] = (float *)malloc(sizeof(float) * APT_PROW_WIDTH); - memcpy(tmpimg.prow[i], img.prow[i], sizeof(float) * APT_PROW_WIDTH); - } - - // Perform visible calibration - apt_calibrate_visible(opts->satnum, &tmpimg, APT_CHA_OFFSET, APT_CH_WIDTH); - ImageOut(opts, &tmpimg, APT_CHA_OFFSET, APT_CH_WIDTH, "Visible", Visible, NULL); - } - - // Linear equalise - if (CONTAINS(opts->effects, Linear_Equalise)) { - apt_linearEnhance(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); - apt_linearEnhance(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); - } - - // Histogram equalise - if (CONTAINS(opts->effects, Histogram_Equalise)) { - apt_histogramEqualise(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); - apt_histogramEqualise(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); - } - - // Raw image - if (CONTAINS(opts->type, Raw_Image)) { - sprintf(desc, "%s (%s) & %s (%s)", ch.id[img.chA], ch.name[img.chA], ch.id[img.chB], ch.name[img.chB]); - ImageOut(opts, &img, 0, APT_IMG_WIDTH, desc, Raw_Image, NULL); - } - - // Palette image - if (CONTAINS(opts->type, Palleted)) { - img.palette = opts->palette; - strcpy(desc, "Palette composite"); - ImageOut(opts, &img, APT_CHA_OFFSET, APT_CH_WIDTH, desc, Palleted, NULL); - } - - // Channel A - if (CONTAINS(opts->type, Channel_A)) { - sprintf(desc, "%s (%s)", ch.id[img.chA], ch.name[img.chA]); - ImageOut(opts, &img, APT_CHA_OFFSET, APT_CH_WIDTH, desc, Channel_A, NULL); - } - - // Channel B - if (CONTAINS(opts->type, Channel_B)) { - sprintf(desc, "%s (%s)", ch.id[img.chB], ch.name[img.chB]); - ImageOut(opts, &img, APT_CHB_OFFSET, APT_CH_WIDTH, desc, Channel_B, NULL); - } - - return 1; -} - -float *samplebuf; -static int initsnd(char *filename) { - SF_INFO infwav; - int res; - - // Open audio file - infwav.format = 0; - audioFile = sf_open(filename, SFM_READ, &infwav); - if (audioFile == NULL) { - error_noexit("Could not file"); - return 0; - } - - res = apt_init(infwav.samplerate); - printf("Input file: %s\n", filename); - if (res < 0) { - error_noexit("Input sample rate too low"); - return 0; - } else if (res > 0) { - error_noexit("Input sample rate too high"); - return 0; - } - printf("Input sample rate: %d\n", infwav.samplerate); - - channels = infwav.channels; - samplebuf = (float *)malloc(sizeof(float) * 32768 * channels); - - return 1; -} - -// Read samples from the audio file -int getsamples(void *context, float *samples, int nb) { - (void)context; - if (channels == 1) { - return (int)sf_read_float(audioFile, samples, nb); - } else if (channels == 2) { - // Stereo channels are interleaved - int samplesRead = (int)sf_read_float(audioFile, samplebuf, nb * channels); - for (int i = 0; i < nb; i++) { - samples[i] = samplebuf[i * channels]; - } - return samplesRead / channels; - } else { - printf("Only mono and stereo input files are supported\n"); - exit(1); - } -} diff --git a/src/pngio.c b/src/pngio.c deleted file mode 100644 index a061b0a..0000000 --- a/src/pngio.c +++ /dev/null @@ -1,411 +0,0 @@ -/* - * This file is part of Aptdec. - * Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 - * - * Aptdec is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "pngio.h" - -#include -#include -#include -#include -#include -#include - -#include "util.h" - -int readRawImage(char *filename, float **prow, int *nrow) { - FILE *fp = fopen(filename, "rb"); - printf("%s", filename); - if (!fp) { - error_noexit("Cannot open image"); - return 0; - } - - // Create reader - png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - if (!png) { - fclose(fp); - return 0; - } - png_infop info = png_create_info_struct(png); - if (!info) { - fclose(fp); - return 0; - } - png_init_io(png, fp); - - // Read info from header - png_read_info(png, info); - int width = png_get_image_width(png, info); - int height = png_get_image_height(png, info); - png_byte color_type = png_get_color_type(png, info); - png_byte bit_depth = png_get_bit_depth(png, info); - - // Check the image - if (width != APT_IMG_WIDTH) { - error_noexit("Raw image must be 2080px wide"); - return 0; - } else if (bit_depth != 8) { - error_noexit("Raw image must have 8 bit color"); - return 0; - } else if (color_type != PNG_COLOR_TYPE_GRAY) { - error_noexit("Raw image must be grayscale"); - return 0; - } - - // Create row buffers - png_bytep *PNGrows = NULL; - PNGrows = (png_bytep *)malloc(sizeof(png_bytep) * height); - for (int y = 0; y < height; y++) PNGrows[y] = (png_byte *)malloc(png_get_rowbytes(png, info)); - - // Read image - png_read_image(png, PNGrows); - - // Tidy up - fclose(fp); - png_destroy_read_struct(&png, &info, NULL); - - // Put into prow - *nrow = height; - for (int y = 0; y < height; y++) { - prow[y] = (float *)malloc(sizeof(float) * width); - - for (int x = 0; x < width; x++) prow[y][x] = (float)PNGrows[y][x]; - } - - return 1; -} - -int readPalette(char *filename, apt_rgb_t **pixels) { - FILE *fp = fopen(filename, "rb"); - if (!fp) { - error_noexit("Cannot open palette"); - return 0; - } - - // Create reader - png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - if (!png) { - fclose(fp); - return 0; - } - png_infop info = png_create_info_struct(png); - if (!info) { - fclose(fp); - return 0; - } - png_init_io(png, fp); - - // Read info from header - png_read_info(png, info); - int width = png_get_image_width(png, info); - int height = png_get_image_height(png, info); - png_byte color_type = png_get_color_type(png, info); - png_byte bit_depth = png_get_bit_depth(png, info); - - // Check the image - if (width != 256 && height != 256) { - error_noexit("Palette must be 256x256"); - return 0; - } else if (bit_depth != 8) { - error_noexit("Palette must be 8 bit color"); - return 0; - } else if (color_type != PNG_COLOR_TYPE_RGB) { - error_noexit("Palette must be RGB"); - return 0; - } - - // Create row buffers - png_bytep *PNGrows = NULL; - PNGrows = (png_bytep *)malloc(sizeof(png_bytep) * height); - for (int y = 0; y < height; y++) PNGrows[y] = (png_byte *)malloc(png_get_rowbytes(png, info)); - - // Read image - png_read_image(png, PNGrows); - - // Tidy up - fclose(fp); - png_destroy_read_struct(&png, &info, NULL); - - // Put into crow - for (int y = 0; y < height; y++) { - pixels[y] = (apt_rgb_t *)malloc(sizeof(apt_rgb_t) * width); - - for (int x = 0; x < width; x++) - pixels[y][x] = (apt_rgb_t){PNGrows[y][x * 3], PNGrows[y][x * 3 + 1], PNGrows[y][x * 3 + 2]}; - } - - return 1; -} - -void prow2crow(float **prow, int nrow, char *palette, apt_rgb_t **crow) { - for (int y = 0; y < nrow; y++) { - crow[y] = (apt_rgb_t *)malloc(sizeof(apt_rgb_t) * APT_IMG_WIDTH); - - for (int x = 0; x < APT_IMG_WIDTH; x++) { - if (palette == NULL) - crow[y][x].r = crow[y][x].g = crow[y][x].b = prow[y][x]; - else - crow[y][x] = apt_applyPalette(palette, prow[y][x]); - } - } -} - -int applyUserPalette(float **prow, int nrow, char *filename, apt_rgb_t **crow) { - apt_rgb_t *pal_row[256]; - if (!readPalette(filename, pal_row)) { - error_noexit("Could not read palette"); - return 0; - } - - for (int y = 0; y < nrow; y++) { - for (int x = 0; x < APT_CH_WIDTH; x++) { - int cha = CLIP(prow[y][x + APT_CHA_OFFSET], 0, 255); - int chb = CLIP(prow[y][x + APT_CHB_OFFSET], 0, 255); - crow[y][x + APT_CHA_OFFSET] = pal_row[chb][cha]; - } - } - - return 1; -} - -int ImageOut(options_t *opts, apt_image_t *img, int offset, int width, char *desc, char chid, char *palette) { - char outName[512]; - if (opts->filename == NULL || opts->filename[0] == '\0') { - sprintf(outName, "%s/%s-%c.png", opts->path, img->name, chid); - } else { - sprintf(outName, "%s/%s", opts->path, opts->filename); - } - - png_text meta[] = {{PNG_TEXT_COMPRESSION_NONE, "Software", VERSION, sizeof(VERSION)}, - {PNG_TEXT_COMPRESSION_NONE, "Channel", desc, sizeof(desc)}, - {PNG_TEXT_COMPRESSION_NONE, "Description", "NOAA satellite image", 20}}; - - // Parse image type - int greyscale = 1; - switch (chid) { - case Palleted: - greyscale = 0; - break; - case Temperature: - greyscale = 0; - break; - case Raw_Image: - break; - case Channel_A: - break; - case Channel_B: - break; - } - - // Parse effects - int crop_telemetry = 0; - for (unsigned long int i = 0; i < strlen(opts->effects); i++) { - switch (opts->effects[i]) { - case Crop_Telemetry: - if (width == 2080) { - width -= APT_TOTAL_TELE; - offset += APT_SYNC_WIDTH + APT_SPC_WIDTH; - crop_telemetry = 1; - } - break; - case Precipitation_Overlay: - greyscale = 0; - break; - case Flip_Image: - break; - case Denoise: - break; - case Histogram_Equalise: - break; - case Linear_Equalise: - break; - case Crop_Noise: - break; - default: { - char text[100]; - sprintf(text, "Unrecognised effect, \"%c\"", opts->effects[i]); - warning(text); - break; - } - } - } - - FILE *pngfile; - - // Create writer - png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - if (!png_ptr) { - png_destroy_write_struct(&png_ptr, (png_infopp)NULL); - error_noexit("Could not create a PNG writer"); - return 0; - } - png_infop info_ptr = png_create_info_struct(png_ptr); - if (!info_ptr) { - png_destroy_write_struct(&png_ptr, (png_infopp)NULL); - error_noexit("Could not create a PNG writer"); - return 0; - } - - if (greyscale) { - // Greyscale image - png_set_IHDR(png_ptr, info_ptr, width, img->nrow, 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, - PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); - } else { - // 8 bit RGB image - png_set_IHDR(png_ptr, info_ptr, width, img->nrow, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); - } - - png_set_text(png_ptr, info_ptr, meta, 3); - png_set_pHYs(png_ptr, info_ptr, 3636, 3636, PNG_RESOLUTION_METER); - - // Init I/O - pngfile = fopen(outName, "wb"); - if (!pngfile) { - error_noexit("Could not open PNG for writing"); - return 1; - } - png_init_io(png_ptr, pngfile); - png_write_info(png_ptr, info_ptr); - - // Move prow into crow, crow ~ color rows, if required - apt_rgb_t *crow[APT_MAX_HEIGHT]; - if (!greyscale) { - prow2crow(img->prow, img->nrow, palette, crow); - } - - // Apply a user provided color palette - if (chid == Palleted) { - applyUserPalette(img->prow, img->nrow, opts->palette, crow); - } - - // Precipitation overlay - if (CONTAINS(opts->effects, Precipitation_Overlay)) { - for (int y = 0; y < img->nrow; y++) { - for (int x = 0; x < APT_CH_WIDTH; x++) { - if (img->prow[y][x + APT_CHB_OFFSET] >= 198) - crow[y][x + APT_CHB_OFFSET] = crow[y][x + APT_CHA_OFFSET] = - apt_applyPalette(apt_PrecipPalette, img->prow[y][x + APT_CHB_OFFSET] - 198); - } - } - } - - printf("Writing %s", outName); - -// Float power macro (for gamma adjustment) -#define POWF(a, b) (b == 1.0 ? a : exp(b * log(a))) - float a = POWF(255, opts->gamma) / 255; - - // Build image - for (int y = 0; y < img->nrow; y++) { - png_color pix[APT_IMG_WIDTH]; // Color - png_byte mpix[APT_IMG_WIDTH]; // Mono - - int skip = 0; - for (int x = 0; x < width; x++) { - if (crop_telemetry && x == APT_CH_WIDTH) skip += APT_TELE_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH; - - if (greyscale) { - mpix[x] = POWF(img->prow[y][x + skip + offset], opts->gamma) / a; - } else { - pix[x] = (png_color){POWF(crow[y][x + skip + offset].r, opts->gamma) / a, - POWF(crow[y][x + skip + offset].g, opts->gamma) / a, - POWF(crow[y][x + skip + offset].b, opts->gamma) / a}; - } - } - - if (greyscale) { - png_write_row(png_ptr, (png_bytep)mpix); - } else { - png_write_row(png_ptr, (png_bytep)pix); - } - } - - // Tidy up - png_write_end(png_ptr, info_ptr); - fclose(pngfile); - printf("\nDone\n"); - png_destroy_write_struct(&png_ptr, &info_ptr); - - return 1; -} - -// TODO: clean up everthing below this comment -png_structp rt_png_ptr; -png_infop rt_info_ptr; -FILE *rt_pngfile; - -int initWriter(options_t *opts, apt_image_t *img, int width, int height, char *desc, char *chid) { - char outName[384]; - sprintf(outName, "%s/%s-%s.png", opts->path, img->name, chid); - - png_text meta[] = {{PNG_TEXT_COMPRESSION_NONE, "Software", VERSION, sizeof(VERSION)}, - {PNG_TEXT_COMPRESSION_NONE, "Channel", desc, sizeof(desc)}, - {PNG_TEXT_COMPRESSION_NONE, "Description", "NOAA satellite image", 20}}; - - // Create writer - rt_png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - if (!rt_png_ptr) { - png_destroy_write_struct(&rt_png_ptr, (png_infopp)NULL); - error_noexit("Could not create a PNG writer"); - return 0; - } - rt_info_ptr = png_create_info_struct(rt_png_ptr); - if (!rt_info_ptr) { - png_destroy_write_struct(&rt_png_ptr, (png_infopp)NULL); - error_noexit("Could not create a PNG writer"); - return 0; - } - - // Greyscale image - png_set_IHDR(rt_png_ptr, rt_info_ptr, width, height, 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); - - png_set_text(rt_png_ptr, rt_info_ptr, meta, 3); - - // Channel = 25cm wide - png_set_pHYs(rt_png_ptr, rt_info_ptr, 3636, 3636, PNG_RESOLUTION_METER); - - // Init I/O - rt_pngfile = fopen(outName, "wb"); - if (!rt_pngfile) { - error_noexit("Could not open PNG for writing"); - return 0; - } - png_init_io(rt_png_ptr, rt_pngfile); - png_write_info(rt_png_ptr, rt_info_ptr); - - // Turn off compression - png_set_compression_level(rt_png_ptr, 0); - - return 1; -} - -void pushRow(float *row, int width) { - png_byte pix[APT_IMG_WIDTH]; - for (int i = 0; i < width; i++) pix[i] = row[i]; - - png_write_row(rt_png_ptr, (png_bytep)pix); -} - -void closeWriter() { - png_write_end(rt_png_ptr, rt_info_ptr); - fclose(rt_pngfile); - png_destroy_write_struct(&rt_png_ptr, &rt_info_ptr); -} diff --git a/src/pngio.h b/src/pngio.h deleted file mode 100644 index f812b3e..0000000 --- a/src/pngio.h +++ /dev/null @@ -1,11 +0,0 @@ -#include "apt.h" -#include "common.h" - -int readRawImage(char *filename, float **prow, int *nrow); -int readPalette(char *filename, apt_rgb_t **pixels); -void prow2crow(float **prow, int nrow, char *palette, apt_rgb_t **crow); -int applyUserPalette(float **prow, int nrow, char *filename, apt_rgb_t **crow); -int ImageOut(options_t *opts, apt_image_t *img, int offset, int width, char *desc, char chid, char *palette); -int initWriter(options_t *opts, apt_image_t *img, int width, int height, char *desc, char *chid); -void pushRow(float *row, int width); -void closeWriter(); diff --git a/src/taps.h b/src/taps.h deleted file mode 100644 index f33b892..0000000 --- a/src/taps.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * aptdec - A lightweight FOSS (NOAA) APT decoder - * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2022 Xerbo (xerbo@protonmail.com) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -static const float hilbert_filter[] = {0.0205361, 0.0219524, 0.0235785, 0.0254648, 0.0276791, 0.0303152, 0.0335063, - 0.0374482, 0.0424413, 0.0489708, 0.0578745, 0.0707355, 0.0909457, 0.127324, - 0.212207, 0.63662, -0.63662, -0.212207, -0.127324, -0.0909457, -0.0707355, - -0.0578745, -0.0489708, -0.0424413, -0.0374482, -0.0335063, -0.0303152, -0.0276791, - -0.0254648, -0.0235785, -0.0219524, -0.0205361}; -#define HILBERT_FILTER_SIZE (sizeof(hilbert_filter) / sizeof(hilbert_filter[0])) - -static const float low_pass[] = { - -3.37279e-04, -8.80292e-06, -3.96418e-04, -1.78544e-04, -5.27511e-04, -3.75376e-04, -6.95337e-04, -5.93148e-04, -8.79730e-04, - -8.15327e-04, -1.05669e-03, -1.01377e-03, -1.19836e-03, -1.15443e-03, -1.26937e-03, -1.20955e-03, -1.23904e-03, -1.15302e-03, - -1.08660e-03, -9.64235e-04, -8.02450e-04, -6.46202e-04, -3.95376e-04, -2.18096e-04, 1.11906e-04, 2.89567e-04, 6.67167e-04, - 8.19039e-04, 1.21725e-03, 1.30556e-03, 1.69365e-03, 1.68588e-03, 2.03277e-03, 1.90159e-03, 2.18455e-03, 1.90833e-03, - 2.12100e-03, 1.69052e-03, 1.77484e-03, 1.42542e-03, 1.18292e-03, 8.66979e-04, 5.54161e-04, 2.15793e-04, -1.11623e-04, - -4.35173e-04, -7.27194e-04, -9.91551e-04, -1.20407e-03, -1.37032e-03, -1.46991e-03, -1.51120e-03, -1.48008e-03, -1.39047e-03, - -1.23115e-03, -1.02128e-03, -7.60099e-04, -4.68008e-04, -1.46339e-04, 1.80867e-04, 5.11244e-04, 8.19243e-04, 1.09739e-03, - 1.32668e-03, 1.50632e-03, 1.61522e-03, 1.66246e-03, 1.62390e-03, 1.52430e-03, 1.34273e-03, 1.10736e-03, 8.10335e-04, - 4.76814e-04, 1.13622e-04, -2.64150e-04, -6.26595e-04, -9.95436e-04, -1.27846e-03, -1.54080e-03, -1.74292e-03, -1.86141e-03, - -1.89318e-03, -1.83969e-03, -1.69770e-03, -1.47938e-03, -1.18696e-03, -8.37003e-04, -4.39507e-04, -1.56907e-05, 4.19904e-04, - 8.43172e-04, 1.23827e-03, 1.58411e-03, 1.86382e-03, 2.06312e-03, 2.17177e-03, 2.18121e-03, 2.08906e-03, 1.89772e-03, - 1.61153e-03, 1.24507e-03, 8.13976e-04, 3.29944e-04, -1.74591e-04, -6.83619e-04, -1.17826e-03, -1.61659e-03, -2.00403e-03, - -2.29070e-03, -2.49179e-03, -2.56546e-03, -2.53448e-03, -2.37032e-03, -2.10060e-03, -1.72140e-03, -1.24542e-03, -7.15425e-04, - -1.24964e-04, 4.83736e-04, 1.08328e-03, 1.64530e-03, 2.14503e-03, 2.55400e-03, 2.85589e-03, 3.02785e-03, 3.06271e-03, - 2.95067e-03, 2.69770e-03, 2.30599e-03, 1.79763e-03, 1.18587e-03, 5.04003e-04, -2.23591e-04, -9.57591e-04, -1.66939e-03, - -2.31717e-03, -2.87636e-03, -3.31209e-03, -3.60506e-03, -3.73609e-03, -3.69208e-03, -3.44913e-03, -3.06572e-03, -2.50229e-03, - -1.80630e-03, -1.00532e-03, -1.22305e-04, 7.83910e-04, 1.69402e-03, 2.53826e-03, 3.30312e-03, 3.91841e-03, 4.38017e-03, - 4.63546e-03, 4.68091e-03, 4.50037e-03, 4.09614e-03, 3.47811e-03, 2.67306e-03, 1.70418e-03, 6.20542e-04, -5.36994e-04, - -1.70981e-03, -2.84712e-03, -3.88827e-03, -4.78659e-03, -5.48593e-03, -5.95049e-03, -6.14483e-03, -6.05118e-03, -5.65829e-03, - -4.97525e-03, -4.01796e-03, -2.82224e-03, -1.43003e-03, 1.00410e-04, 1.71169e-03, 3.31983e-03, 4.87796e-03, 6.23237e-03, - 7.31013e-03, 8.20642e-03, 8.67374e-03, 8.77681e-03, 8.43444e-03, 7.66794e-03, 6.46827e-03, 4.87294e-03, 2.92923e-03, - 6.98913e-04, -1.72126e-03, -4.24785e-03, -6.75380e-03, -9.13309e-03, -1.12532e-02, -1.30038e-02, -1.42633e-02, -1.49338e-02, - -1.49145e-02, -1.41484e-02, -1.25761e-02, -1.01870e-02, -6.97432e-03, -2.97910e-03, 1.75386e-03, 7.11899e-03, 1.30225e-02, - 1.93173e-02, 2.58685e-02, 3.24965e-02, 3.90469e-02, 4.53316e-02, 5.11931e-02, 5.64604e-02, 6.09924e-02, 6.46584e-02, - 6.73547e-02, 6.90049e-02, 6.97096e-02, 6.90049e-02, 6.73547e-02, 6.46584e-02, 6.09924e-02, 5.64604e-02, 5.11931e-02, - 4.53316e-02, 3.90469e-02, 3.24965e-02, 2.58685e-02, 1.93173e-02, 1.30225e-02, 7.11899e-03, 1.75386e-03, -2.97910e-03, - -6.97432e-03, -1.01870e-02, -1.25761e-02, -1.41484e-02, -1.49145e-02, -1.49338e-02, -1.42633e-02, -1.30038e-02, -1.12532e-02, - -9.13309e-03, -6.75380e-03, -4.24785e-03, -1.72126e-03, 6.98913e-04, 2.92923e-03, 4.87294e-03, 6.46827e-03, 7.66794e-03, - 8.43444e-03, 8.77681e-03, 8.67374e-03, 8.20642e-03, 7.31013e-03, 6.23237e-03, 4.87796e-03, 3.31983e-03, 1.71169e-03, - 1.00410e-04, -1.43003e-03, -2.82224e-03, -4.01796e-03, -4.97525e-03, -5.65829e-03, -6.05118e-03, -6.14483e-03, -5.95049e-03, - -5.48593e-03, -4.78659e-03, -3.88827e-03, -2.84712e-03, -1.70981e-03, -5.36994e-04, 6.20542e-04, 1.70418e-03, 2.67306e-03, - 3.47811e-03, 4.09614e-03, 4.50037e-03, 4.68091e-03, 4.63546e-03, 4.38017e-03, 3.91841e-03, 3.30312e-03, 2.53826e-03, - 1.69402e-03, 7.83910e-04, -1.22305e-04, -1.00532e-03, -1.80630e-03, -2.50229e-03, -3.06572e-03, -3.44913e-03, -3.69208e-03, - -3.73609e-03, -3.60506e-03, -3.31209e-03, -2.87636e-03, -2.31717e-03, -1.66939e-03, -9.57591e-04, -2.23591e-04, 5.04003e-04, - 1.18587e-03, 1.79763e-03, 2.30599e-03, 2.69770e-03, 2.95067e-03, 3.06271e-03, 3.02785e-03, 2.85589e-03, 2.55400e-03, - 2.14503e-03, 1.64530e-03, 1.08328e-03, 4.83736e-04, -1.24964e-04, -7.15425e-04, -1.24542e-03, -1.72140e-03, -2.10060e-03, - -2.37032e-03, -2.53448e-03, -2.56546e-03, -2.49179e-03, -2.29070e-03, -2.00403e-03, -1.61659e-03, -1.17826e-03, -6.83619e-04, - -1.74591e-04, 3.29944e-04, 8.13976e-04, 1.24507e-03, 1.61153e-03, 1.89772e-03, 2.08906e-03, 2.18121e-03, 2.17177e-03, - 2.06312e-03, 1.86382e-03, 1.58411e-03, 1.23827e-03, 8.43172e-04, 4.19904e-04, -1.56907e-05, -4.39507e-04, -8.37003e-04, - -1.18696e-03, -1.47938e-03, -1.69770e-03, -1.83969e-03, -1.89318e-03, -1.86141e-03, -1.74292e-03, -1.54080e-03, -1.27846e-03, - -9.95436e-04, -6.26595e-04, -2.64150e-04, 1.13622e-04, 4.76814e-04, 8.10335e-04, 1.10736e-03, 1.34273e-03, 1.52430e-03, - 1.62390e-03, 1.66246e-03, 1.61522e-03, 1.50632e-03, 1.32668e-03, 1.09739e-03, 8.19243e-04, 5.11244e-04, 1.80867e-04, - -1.46339e-04, -4.68008e-04, -7.60099e-04, -1.02128e-03, -1.23115e-03, -1.39047e-03, -1.48008e-03, -1.51120e-03, -1.46991e-03, - -1.37032e-03, -1.20407e-03, -9.91551e-04, -7.27194e-04, -4.35173e-04, -1.11623e-04, 2.15793e-04, 5.54161e-04, 8.66979e-04, - 1.18292e-03, 1.42542e-03, 1.77484e-03, 1.69052e-03, 2.12100e-03, 1.90833e-03, 2.18455e-03, 1.90159e-03, 2.03277e-03, - 1.68588e-03, 1.69365e-03, 1.30556e-03, 1.21725e-03, 8.19039e-04, 6.67167e-04, 2.89567e-04, 1.11906e-04, -2.18096e-04, - -3.95376e-04, -6.46202e-04, -8.02450e-04, -9.64235e-04, -1.08660e-03, -1.15302e-03, -1.23904e-03, -1.20955e-03, -1.26937e-03, - -1.15443e-03, -1.19836e-03, -1.01377e-03, -1.05669e-03, -8.15327e-04, -8.79730e-04, -5.93148e-04, -6.95337e-04, -3.75376e-04, - -5.27511e-04, -1.78544e-04, -3.96418e-04, -8.80292e-06, -3.37279e-04}; -#define LOW_PASS_SIZE (sizeof(low_pass) / sizeof(low_pass[0])) - -static const float sync_pattern[] = {-14, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, 18, - 18, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, -14}; -#define SYNC_PATTERN_SIZE (sizeof(sync_pattern) / sizeof(sync_pattern[0])) diff --git a/util/PrecipitationPalette.png b/util/PrecipitationPalette.png index 2ac2db0d4ad1cea1b77eac2bfc3669f66019087c..943868ec01c32fc61f69e58c155f18f38e6bcc78 100644 GIT binary patch delta 14 VcmX@ceVlQE@??HivB?&!eE=lH1ZV&N delta 1309 zcmV+&1>*Y00mciEBYy)=dQ@0+Qek%>aB^>EX>4U6ba`-PAZ2)IW&i+q+O3ssjvOfr zhX2V)TMu?n!%XkZ1lSUw_^`6n(Kk`e}o_&G4^= z&7XNr{F_H*zPEgY^ZX)ajaYYdo(s}Y?x1cH*GmlqJA#(8t)Llxif=ouq1EJRP);+A z)X@etVv-b;XMK(7zKFBj{E1V$#f>cP93MFGUqAXki$l*Cl z<_-d+$`ej<7Aas;QAerJMg3T=s3BL;tg1y#t2Xtk zNXbP@DOUEGa}&#^R?MtgGp|x})mp06T3hwTO>iW&m1eEB*4$y!^X2N;?T~Qq+@#ofx9$VwGxE?;hK)Mf@X1qVo;u64S!bKRXrYv@m6xuvY}M742Wl-Oa%hyWsL{gT z)aW_3pMTqbphgchKBTssy{Tc=Y%4k{Xx}rsnSmH*0&&{}P|&=YrAj&TCbyZT#yAzq za8fs$Ny#rNK$sV?2EEyRkoz{b3%ULqH~tQ}Yoq%g$nmx06Sps@&3zM&L+n0g;WoR( z!usm?@3KI4D69uA0BH2+V`W*_?Dysq8iHv9IoejCxWKAI)?L=vX_d7KMVm;>QHxS| z4S)6@WN&Naou@Tn8XL0;Ze@6QB3M;+LKk)*KEquhO9COxgis~!Krf#&`thOL&Q>0% zy>zDT2|e=h$b;We952NMD_h(3T1G{*74XIP2vhu>ero~xpQ#x3P1n1nlK=n#g=s@W zP)S2WAaHVTW@&6?004NLeUUv#!$2IxUw>O`MJft*5OK&*oh*ooh?7>K2o*xD(5i#U zrC-pbAxUv@6kH1qek@iUT%2`va1{i>4-h9uCq)-2@qbC7MT`f>{djlparX}J*UL;b zJH`N2vy4FbDic85?I6%LkMG0kCh|#K%Vj@NRaS#8X z;}^*#ldA+qjs;YpLUR1zfAG6oGk-rZ?k0ufK=8%3KZb$OF3_mi_V=-EH%@@SGjOG~ z{FOQ|{YiSQrA3c`-fiIGx~0i`z~v4w@T5zIUA*>z#n&VLU*@>4HJ7xwHQxse?? z0X>Yu`Men3qqbMS^?R7!f^MT~)XW~Hx8Zw*K8<{@pk~wSEsh{SYSfJ8jEfTG~Sa7#Z-hA@q886 zo0uZ!dd}@bv){e@9gz#_16_Day@^=UA2@kd5%Z{#JusF#GmB{veHTxE#HU4EbpI!( hKBJ!cRidSr^cO^3fy<w|yT`Y)-2rgQMB2);qLaPoYmwrK$h9t$sQE)9d__0`ZaBh822neDNBbbqy zWz0!Z0>0zx9s$1IMR}J0xj#p@nza}Z5Q%4*VcNtS#8aEL!FiuJ%nGtfd`>)S(gleh zxvqHp#yRh@z%#>UDmhOaCKmH8th6vIm>TgUaYWU0$`{fetDLtuYo!Wn+>^gBl+{<3 zxlVHsaV%m95`RP}sGx`vY(#0*NwJWm{g{t`!1YVyQpi;VBgX>D&>*{h@IUz7t(BV? z_mV;}p#8;hK8ApfU7%idobO}Dsh2ve$jy-PzjPzh@f# z{Q#lxa=KPDOWXhe00v@9M??TZ02~1NlO*v#ks%m=lu1NER5*=ok;`t>KoCVwpDJ4( z!f)|?d{q=B5}H+5^VowB8!~=03s3jNfz8pad#kETHXry{Er^9eQ#S(7u({*;m*ITZu+7=XR29kk-^!Ea}9n%b%?t#6-*R;J)kzz8t9m)X30GtuwcXY-KmK}Qr&!2}FIOINg; zT^ptXGJ}t2@cax;*(YkB*jnj!@S+}&TMuriV644d!Cbm`!_jjA^U?x)i}OKw0jC)( vtQ+|72mCaHCk`JXdie&!v2Oqzy9e+WNqU0HCBBG400000NkvXXu0mjfH(ifA diff --git a/util/Temperature.png b/util/Temperature.png index 012f57ff721b44f058a8c096359dad3cd7ed8436..fa5bb0e0ebd18eb3454ea8f4a268706ea658158c 100644 GIT binary patch delta 11 Scmcb|zlUdn^5&zAl57ARw*&zI delta 430 zcmV;f0a5EX>4Tx04R}tkv&MmKpe$iQ%j{(9PA+C zkfAzR5fydRDionYsTEpvFuC*#nlvOSE{=k0!NHHks)LKOt`4q(Aou~|}?mh0_0Yam~RI_UgP&La)C*oo@w<-o-As~PdMt?CaF;kyQrIPR*U-$6w z^)AM|nWrS;by?xO#aXS?SnHnrh2gxmvdndwgGgWzOGrV4 zj2gUH6XR}DI1U6}Z2Myf z2<`&Sx@~_S+jjE=@IM1rTH9Z305hMY*V|h32@ctJ^&f&DtQAO90J2d%3k+)cO!RaZ~vZY_4fmiZ*sF8DkEtC Y000JJOGiWi{{a60|De66ld&Ni20>cJ@&Et; diff --git a/util/img2gradient.py b/util/img2gradient.py new file mode 100755 index 0000000..815cda7 --- /dev/null +++ b/util/img2gradient.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +import sys +from PIL import Image + +""" +Converts a PNG into a gradient compatible +with aptdec. Requires Pillow: + + pip3 install Pillow + +""" + +if len(sys.argv) == 1: + print("Usage: {} filename.png".format(sys.argv[0])) + exit() + +image = Image.open(sys.argv[1]) +pixels = image.load() + +if len(pixels[0, 0]) != 3: + print("Image must be RGB") + exit() + +if image.size[0] != 1: + print("Image must be 1px wide") + exit() + +print("uint32_t gradient[{}] = {{\n ".format(image.size[1]), end="") +for y in range(image.size[1]): + print("0x" + "".join("{:02X}".format(a) for a in pixels[0, y]), end="") + + if y != image.size[1] - 1: + print(", ", end="") + if y % 7 == 6: + print("\n ", end="") + +print("\n};") diff --git a/util/img2pal.py b/util/img2pal.py deleted file mode 100644 index f5c3753..0000000 --- a/util/img2pal.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/python3 -import sys -from PIL import Image - -''' -Converts a PNG into a palette compatible -with aptdec. Requires PIL: - - pip3 install Pillow - -''' - -if len(sys.argv) == 1: - print("Usage: python3 {} filename.png".format(sys.argv[0])) - exit() - -image = Image.open(sys.argv[1]) -pixels = image.load() - -sys.stdout.write("char palette[{}*3] = {{\n \"".format(image.size[1])) -for y in range(1, image.size[1]+1): - sys.stdout.write(''.join('\\x{:02x}'.format(a) for a in pixels[0, y-1])) - if(y % 7 == 0 and y != 0): - sys.stdout.write("\"\n \"") - -print("\"\n};")