@@ -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 | # Object files | ||||
*.o | *.o | ||||
@@ -26,6 +30,7 @@ | |||||
*.dll | *.dll | ||||
*.so | *.so | ||||
*.so.* | *.so.* | ||||
*.dylib | |||||
# Executables | # Executables | ||||
*.exe | *.exe | ||||
@@ -33,6 +38,7 @@ | |||||
*.app | *.app | ||||
*.i*86 | *.i*86 | ||||
*.x86_64 | *.x86_64 | ||||
*.hex | |||||
# Debug files | # Debug files | ||||
*.dSYM/ | *.dSYM/ | ||||
@@ -40,19 +46,40 @@ | |||||
*.idb | *.idb | ||||
*.pdb | *.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 | *.png | ||||
!textlogo.png | !textlogo.png | ||||
!palettes/*.png | |||||
!luts/*.png | |||||
!util/*.png | !util/*.png | ||||
*.wav | *.wav | ||||
aptdec | |||||
*.ogg | |||||
.vscode/ | |||||
build/ | build/ | ||||
libpng/ | |||||
zlib/ | |||||
winbuild/ | |||||
winpath/ | |||||
libsndfile-1.0.29-win64.zip | |||||
libsndfile-1.0.29-win64/ |
@@ -1,3 +1,3 @@ | |||||
[submodule "src/argparse"] | |||||
path = src/argparse | |||||
[submodule "aptdec-cli/argparse"] | |||||
path = aptdec-cli/argparse | |||||
url = https://github.com/cofyc/argparse | url = https://github.com/cofyc/argparse |
@@ -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) | include(GNUInstallDirs) | ||||
# libpng | |||||
# aptdec-cli | |||||
find_package(PNG) | 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() | 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() | endif() | ||||
else() | 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() | 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() | 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() | 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_NAME "aptdec") | ||||
set(CPACK_PACKAGE_CONTACT "Xerbo (xerbo@protonmail.com)") | set(CPACK_PACKAGE_CONTACT "Xerbo (xerbo@protonmail.com)") | ||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "NOAA APT satellite imagery decoder") | 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) | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) | ||||
IF(NOT WIN32) | |||||
if(NOT WIN32) | |||||
set(CPACK_GENERATOR "DEB;TGZ") | set(CPACK_GENERATOR "DEB;TGZ") | ||||
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}.${CMAKE_SYSTEM_PROCESSOR}") | set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}.${CMAKE_SYSTEM_PROCESSOR}") | ||||
else() | else() | ||||
@@ -89,11 +94,10 @@ else() | |||||
endif() | endif() | ||||
endif() | endif() | ||||
if (TARGET aptdec) | |||||
if(TARGET aptdec-cli) | |||||
install(TARGETS aptdec RUNTIME DESTINATION bin) | install(TARGETS aptdec RUNTIME DESTINATION bin) | ||||
install(DIRECTORY "${PROJECT_SOURCE_DIR}/palettes" DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}) | |||||
endif() | 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) | include(CPack) |
@@ -1,28 +1,27 @@ | |||||
# How To Contribute | # 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 | ## 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 | ## 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 |
@@ -6,58 +6,63 @@ Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 20 | |||||
## Introduction | ## 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 | ## Quick start | ||||
Grab a release from [Releases](https://github.com/Xerbo/aptdec/releases) or compile from source: | |||||
```sh | ```sh | ||||
sudo apt install cmake git gcc libsndfile-dev libpng-dev | sudo apt install cmake git gcc libsndfile-dev libpng-dev | ||||
git clone --recursive https://github.com/Xerbo/aptdec.git && cd aptdec | git clone --recursive https://github.com/Xerbo/aptdec.git && cd aptdec | ||||
cmake -B build | cmake -B build | ||||
cmake --build build | cmake --build build | ||||
# Resulting binary is build/aptdec | |||||
# Resulting binary is build/aptdec-cli | |||||
``` | ``` | ||||
In place builds are not supported. | |||||
## Examples | ## 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 | ```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 | ```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 | ```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) | Apply a denoise filter (see [Post-Processing Effects](#post-processing-effects) for a full list of post-processing effects) | ||||
```sh | ```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 | ```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 | ```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 | ## Usage | ||||
@@ -65,72 +70,74 @@ Apply a falsecolor palette | |||||
### Arguments | ### Arguments | ||||
``` | ``` | ||||
-i [r|a|b|t|m|p] Output type (stackable) | |||||
-e [t|h|l|d|p|f] Effects (stackable) | |||||
-o <path> Output filename | |||||
-d <path> Destination directory | |||||
-s (15-19) Satellite number | |||||
-p <path> Path to palette | |||||
-r Realtime decode | |||||
-g Gamma adjustment (1.0 = off) | |||||
-h, --help show a help message and exit | |||||
-i, --image=<str> set output image type (see below) | |||||
-e, --effect=<str> add an effect (see below) | |||||
-g, --gamma=<flt> gamma adjustment (1.0 = off) | |||||
-s, --satellite=<int> satellite ID, must be between 15, 18 or 19 or NORAD | |||||
-l, --lut=<str> path to a LUT | |||||
-o, --output=<str> path of output image | |||||
-r, --realtime decode in realtime | |||||
``` | ``` | ||||
### Image output types | ### 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 | ### 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 | ## 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 | ## 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 | 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 | .\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 | ## License | ||||
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#ifndef _MSC_VER | |||||
#include <libgen.h> | |||||
#else | |||||
#include <windows.h> | |||||
#endif | |||||
#include <math.h> | |||||
#include <sndfile.h> | |||||
#include <time.h> | |||||
#include <signal.h> | |||||
#include <float.h> | |||||
#include <aptdec.h> | |||||
#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 | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include "pngio.h" | |||||
#include <png.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#ifndef APTDEC_CLI_PNGIO_H_ | |||||
#define APTDEC_CLI_PNGIO_H_ | |||||
#include <aptdec.h> | |||||
#include <png.h> | |||||
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 |
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* aptdec - A lightweight FOSS (NOAA) APT decoder | * 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 | * 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 | * it under the terms of the GNU General Public License as published by | ||||
@@ -18,6 +18,7 @@ | |||||
#include "util.h" | #include "util.h" | ||||
#include <aptdec.h> | |||||
#include <stdio.h> | #include <stdio.h> | ||||
#include <stdlib.h> | #include <stdlib.h> | ||||
@@ -28,10 +29,12 @@ void error_noexit(const char *text) { | |||||
fprintf(stderr, "\033[31mError: %s\033[0m\n", text); | fprintf(stderr, "\033[31mError: %s\033[0m\n", text); | ||||
#endif | #endif | ||||
} | } | ||||
void error(const char *text) { | void error(const char *text) { | ||||
error_noexit(text); | error_noexit(text); | ||||
exit(1); | exit(1); | ||||
} | } | ||||
void warning(const char *text) { | void warning(const char *text) { | ||||
#ifdef _WIN32 | #ifdef _WIN32 | ||||
fprintf(stderr, "Warning: %s\r\n", text); | fprintf(stderr, "Warning: %s\r\n", text); | ||||
@@ -40,10 +43,12 @@ void warning(const char *text) { | |||||
#endif | #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 > hi) return hi; | ||||
if (x < lo) return lo; | if (x < lo) return lo; | ||||
return x; | 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()); | |||||
} |
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* aptdec - A lightweight FOSS (NOAA) APT decoder | * 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 | * 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 | * it under the terms of the GNU General Public License as published by | ||||
@@ -16,19 +16,17 @@ | |||||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
*/ | */ | ||||
#include <stdio.h> | |||||
#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 <stddef.h> | |||||
#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); | void error_noexit(const char *text); | ||||
__attribute__((noreturn)) void error(const char *text); | |||||
void warning(const char *text); | void warning(const char *text); | ||||
int clamp_int(int x, int lo, int hi); | |||||
int get_version(char *str); | |||||
#endif | #endif |
@@ -2,7 +2,7 @@ REM Build using Visual Studio 2019 on Windows | |||||
REM Additional tools needed: git, cmake and ninja | REM Additional tools needed: git, cmake and ninja | ||||
REM Build zlib | REM Build zlib | ||||
git clone https://github.com/madler/zlib | |||||
git clone -b v1.2.13 https://github.com/madler/zlib | |||||
cd zlib | cd zlib | ||||
mkdir build | mkdir build | ||||
cd build | cd build | ||||
@@ -11,7 +11,7 @@ ninja install | |||||
cd ../../ | cd ../../ | ||||
REM Build libpng | REM Build libpng | ||||
git clone https://github.com/glennrp/libpng | |||||
git clone -b v1.6.39 https://github.com/glennrp/libpng | |||||
cd libpng | cd libpng | ||||
mkdir build | mkdir build | ||||
cd build | cd build | ||||
@@ -20,7 +20,7 @@ ninja install | |||||
cd ../.. | cd ../.. | ||||
REM Build libsndfile - Could build Vorbis, FLAC and Opus first for extra support | 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 | cd libsndfile | ||||
mkdir build | mkdir build | ||||
cd build | cd build | ||||
@@ -1,4 +1,4 @@ | |||||
#!/bin/bash | |||||
#!/usr/bin/env bash | |||||
# Cross compile for Windows from Linux | # Cross compile for Windows from Linux | ||||
TEMP_PATH="$(pwd)/winpath" | 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/include/sndfile.h" $TEMP_PATH/include/sndfile.h | ||||
cp "libsndfile-1.0.29-win64/lib/sndfile.lib" $TEMP_PATH/lib/sndfile.lib | cp "libsndfile-1.0.29-win64/lib/sndfile.lib" $TEMP_PATH/lib/sndfile.lib | ||||
# Copy DLL's into root for CPack | # Copy DLL's into root for CPack | ||||
cp $TEMP_PATH/bin/*.dll ../ | cp $TEMP_PATH/bin/*.dll ../ | ||||
@@ -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) |
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* aptdec - A lightweight FOSS (NOAA) APT decoder | * 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 | * 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 | * it under the terms of the GNU General Public License as published by | ||||
@@ -18,6 +18,7 @@ | |||||
#include "algebra.h" | #include "algebra.h" | ||||
#include <stdlib.h> | |||||
#include <math.h> | #include <math.h> | ||||
// Find the best linear equation to estimate the value of the | // 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}; | 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); | |||||
} |
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* aptdec - A lightweight FOSS (NOAA) APT decoder | * 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 | * 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 | * it under the terms of the GNU General Public License as published by | ||||
@@ -16,10 +16,13 @@ | |||||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
*/ | */ | ||||
#ifndef APTDEC_ALGEBRA_H | |||||
#define APTDEC_ALGEBRA_H | |||||
#ifndef LIBAPTDEC_ALGEBRA_H_ | |||||
#define LIBAPTDEC_ALGEBRA_H_ | |||||
#include <stddef.h> | #include <stddef.h> | ||||
#define M_PIf 3.14159265358979323846f | |||||
#define M_TAUf (M_PIf * 2.0f) | |||||
// A linear equation in the form of y(x) = ax + b | // A linear equation in the form of y(x) = ax + b | ||||
typedef struct { | typedef struct { | ||||
float a, b; | float a, b; | ||||
@@ -31,8 +34,16 @@ typedef struct { | |||||
} quadratic_t; | } quadratic_t; | ||||
linear_t linear_regression(const float *independent, const float *dependent, size_t len); | 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 linear_calc(float x, linear_t line); | ||||
float quadratic_calc(float x, quadratic_t quadratic); | float quadratic_calc(float x, quadratic_t quadratic); | ||||
float sincf(float x); | |||||
#endif | #endif |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#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]; | |||||
} | |||||
} |
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* aptdec - A lightweight FOSS (NOAA) APT decoder | * 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 | * 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 | * it under the terms of the GNU General Public License as published by | ||||
@@ -16,9 +16,11 @@ | |||||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
*/ | */ | ||||
#ifndef APTDEC_CALIBRATION_H | |||||
#define APTDEC_CALIBRATION_H | |||||
#ifndef LIBAPTDEC_CALIBRATION_H_ | |||||
#define LIBAPTDEC_CALIBRATION_H_ | |||||
#include "algebra.h" | #include "algebra.h" | ||||
#include <aptdec.h> | |||||
typedef struct { | typedef struct { | ||||
char *name; | char *name; | ||||
@@ -50,6 +52,6 @@ static const float C1 = 1.1910427e-5f; | |||||
// Second radiation constant (cm-K) | // Second radiation constant (cm-K) | ||||
static const float C2 = 1.4387752f; | static const float C2 = 1.4387752f; | ||||
calibration_t get_calibration(int satid); | |||||
calibration_t get_calibration(apt_satellite_t satid); | |||||
#endif | #endif |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include <aptdec.h> | |||||
#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 | |||||
}; |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include <float.h> | |||||
#include <math.h> | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#include <aptdec.h> | |||||
#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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include <aptdec.h> | |||||
#include <string.h> | |||||
#include <stdio.h> | |||||
#include <math.h> | |||||
#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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include "filter.h" | |||||
#include <math.h> | |||||
// SSE2 intrinsics | |||||
#ifdef __x86_64__ | |||||
#include <emmintrin.h> | |||||
#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); | |||||
} |
@@ -1,6 +1,6 @@ | |||||
/* | /* | ||||
* aptdec - A lightweight FOSS (NOAA) APT decoder | * 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 | * 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 | * it under the terms of the GNU General Public License as published by | ||||
@@ -16,15 +16,27 @@ | |||||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
*/ | */ | ||||
#ifndef LIBAPTDEC_FILTER_H_ | |||||
#define LIBAPTDEC_FILTER_H_ | |||||
#include <complex.h> | #include <complex.h> | ||||
#include <stddef.h> | #include <stddef.h> | ||||
#ifdef _MSC_VER | #ifdef _MSC_VER | ||||
typedef _Fcomplex complexf_t; | typedef _Fcomplex complexf_t; | ||||
#define complex_build(real, imag) _FCbuild(real, imag) | |||||
#define complex_multiply(a, b) _FCmulcc(a, b) | |||||
#else | #else | ||||
typedef complex float complexf_t; | typedef complex float complexf_t; | ||||
#define complex_build(real, imag) ((real) + (imag)*I) | |||||
#define complex_multiply(a, b) ((a) * (b)) | |||||
#endif | #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); | float convolve(const float *in, const float *taps, size_t len); | ||||
complexf_t hilbert_transform(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 |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include <math.h> | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#include <float.h> | |||||
#include "algebra.h" | |||||
#include <aptdec.h> | |||||
#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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#ifndef APTDEC_H_ | |||||
#define APTDEC_H_ | |||||
#include <stddef.h> | |||||
#include <stdint.h> | |||||
#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 |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include "util.h" | |||||
#include <stdlib.h> | |||||
#include <math.h> | |||||
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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#ifndef LIBAPTDEC_UTIL_H_ | |||||
#define LIBAPTDEC_UTIL_H_ | |||||
#include <stdint.h> | |||||
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 |
@@ -1,126 +0,0 @@ | |||||
/* | |||||
* This file is part of Aptdec. | |||||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||||
* Copyright (c) 2021 Jon Beniston (M7RCE) | |||||
* | |||||
* 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 <https://www.gnu.org/licenses/>. | |||||
* | |||||
*/ | |||||
#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 |
@@ -1 +0,0 @@ | |||||
Subproject commit c612dc03958cdbd538ca306d61853b643a435933 |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#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]; | |||||
} | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
* | |||||
*/ | |||||
#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"}; |
@@ -1,3 +0,0 @@ | |||||
#include "common.h" | |||||
#define MCOMPOSITE(m1, a1, m2, a2) (m1 * a1 + m2 * a2 * (1 - a1)) |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
* | |||||
*/ | |||||
// 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 |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include <math.h> | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
#include "filter.h" | |||||
#include <math.h> | |||||
#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; | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
* | |||||
*/ | |||||
#include "image.h" | |||||
#include <math.h> | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#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); | |||||
} | |||||
} | |||||
} |
@@ -1,2 +0,0 @@ | |||||
#include "apt.h" | |||||
#include "common.h" |
@@ -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 |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
* | |||||
*/ | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#ifndef _MSC_VER | |||||
#include <libgen.h> | |||||
#else | |||||
#include <windows.h> | |||||
#endif | |||||
#include <errno.h> | |||||
#include <math.h> | |||||
#include <sndfile.h> | |||||
#include <time.h> | |||||
#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); | |||||
} | |||||
} |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
* | |||||
*/ | |||||
#include "pngio.h" | |||||
#include <math.h> | |||||
#include <png.h> | |||||
#include <stdint.h> | |||||
#include <stdio.h> | |||||
#include <stdlib.h> | |||||
#include <string.h> | |||||
#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); | |||||
} |
@@ -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(); |
@@ -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 <https://www.gnu.org/licenses/>. | |||||
*/ | |||||
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])) |
@@ -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};") |
@@ -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};") |